feat(power-agent): add DCGM dual actuator (opt-in; NVML remains default) - #9790
Conversation
WalkthroughThis pull request introduces a complete Power Agent DaemonSet for enforcing per-GPU power caps on Dynamo worker nodes, featuring a pluggable dual-actuator design (NVML default, DCGM opt-in), comprehensive test coverage, and production-ready Helm chart deployment with template-time validation. ChangesPower Agent Dual-Actuator System
🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Aligns docs/design-docs/power-agent-helm-chart-plan.md with the v1.1.0 chart that PR #9790 ships. The plan was authored for the NVML-only v1.0.0 chart in PR #9682 and went stale once PR #9790 layered agent.actuator + agent.dcgm.* + validateActuator + validateEnforce + the helm-unittest suite on top. Two changelog rows capture the refresh: v1.3 - first pass: 8 reviewer findings closed (3 blocking, 4 major, 2 medium, 1 low) on Status header, values surface, dev ConfigMap recipe, image-tag pinning, helm-unittest gating, internal-dev-doc references, daemonset name, file/LOC accounting. v1.4 - second pass: 5 follow-up findings closed (2 major, 2 medium, 1 low) on Status self-contradiction, stale section 4.2 dev-block comment, overstated helm-unittest coverage prose, unrunnable section 5.4 positive helm template overlays (missing --set image.tag), and the lingering filename reference in the v1.3 changelog. No design reversal: every section 6 decision and the chart shape are unchanged. Only the values surface (extended), helper set (extended with two template-time validators), and validation-gate list (helm-unittest now required) grew. Every claim in the refreshed doc was verified against on-disk state: power_agent.py:706-804 for the 8-flag CLI surface, values.yaml for the dev-block recipe, _helpers.tpl for the five-helper set, and the two tests/validate_*_test.yaml files for the 24 enumerated unittest cases. Signed-off-by: Kai Ma <kaim@nvidia.com>
Aligns docs/design-docs/power-agent-helm-chart-plan.md with the v1.2.0 chart that the preceding commit ships. The plan was authored for the v1.0.0 NVML-only chart in PR #9682, refreshed to v1.1.0 in the PR #9790 dual-actuator work, and went stale again once v1.2.0 landed image.digest + the canonical-OCI imageRef helper on top. Four reviewer findings closed (v1.5 changelog row covers all four): (1) Truncated SHA-256 digests slipped through the v1.1.0 helper - already fixed in the preceding chart commit; this commit propagates the rule into the §4.4 helper snippet and the §5.4 validation gates. (2) Whitespace-padded tags rendered raw image references - same treatment: §4.4 documents the trim-then-reject contract. (3) §4.3 / §4.4 / §5.4 stale on image.digest: - §4.3 CodeRabbit-comment row rewritten to describe both helpers (validateImageTag + imageRef), both fields (tag + digest), the canonical repo@digest form, and the PR9682 follow-up that added the separate field. - §4.4 helper snippet replaced with the actual v1.2.0 validator (full rule set + per-rule rationale comments) plus the new imageRef helper. - §5.4 expected helm-unittest output bumped 24 -> 46 passed with a one-line breakdown of the +22 new cases. (4) §4.1 / §4.2 stale on file count + helper list + values surface: - §4.1 said 13 files (omitted .helmignore and the new validate_image_tag_test.yaml). Corrected to 14 files (7 templates + 3 helm-unittests + 4 root files). The _helpers.tpl helper-list line gained imageRef and chart entries. The daemonset.yaml annotation now mentions it routes through imageRef. LOC estimate bumped ~1,430 -> ~1,700 with a breakdown of what v1.2.0 added on top of v1.0.0 / v1.1.0. - §4.2 values snippet had only image.tag with no image.digest. Added the field with the same per-rule comment block that ships in values.yaml (OCI form, 64-hex requirement, mutex with image.tag, PR9682 rationale). Status header bumped to chart v1.2.0 with a one-line v1.2.0 rationale (additive opt-in field, no breaking change to existing tag-pinned installs). Revision-history table gains the v1.5 entry summarising all four findings; older v1.1 - v1.4 entries untouched. No design reversal: every §6 decision and the chart shape are unchanged. Only the values surface (one additive field), helper set (two helpers: imageRef new, validateImageTag rewritten), and helm-unittest count (24 -> 46) grew. Every claim in the refreshed doc was verified against on-disk state: Chart.yaml for the version bump, values.yaml for the image.digest field comment, _helpers.tpl for both helpers, tests/validate_image_tag_test.yaml for the 22 enumerated cases (verified via `helm unittest`). Signed-off-by: Kai Ma <kaim@nvidia.com>
…ttributes (v1.10 e2e fix) First in-cluster DCGM parity run on an 8xA100 SXM node (PR #9790 e2e) surfaced three production defects in DcgmActuator, all rooted in pydcgm API misuse that the mocked unit suite couldn't catch: 1. `get_uuid` and `_ensure_identity_map._read_dcgm_uuids` read `DCGM_FI_DEV_UUID` via `dcgmEntityGetLatestValues`. That API returns the field cache; on a fresh nv-hostengine 4.5.3 with no companion watcher subscribed via `dcgmWatchFields`, the cache returns `DCGM_STR_BLANK = "<<<NULL>>>"`. The identity map then raised "8 GPU UUID(s) visible to DCGM are not visible to NVML" on every cluster startup. 2. `constraints_w` / `current_w` / `default_w` read the four `DCGM_FI_DEV_POWER_MGMT_LIMIT{,_MIN,_MAX,_DEF}` fields the same way. Every read returned `DCGM_FP64_BLANK = 2^47`, so the clamp in `apply_cap` escalated a requested 250 W up to the blank max and tried to write 140737488355328 W. 3. The workload-power-profile blanking loop in `_apply_cap_inner` reached for `dcgm_structs.DCGM_INT32_BLANK`. The constant actually lives in `dcgmvalue` (`/shared/pydcgm/dcgmvalue.py:17` in the DCGM 4.5.3 apt bindings); pre-v1.10 the first cap write raised `AttributeError: module 'dcgm_structs' has no attribute 'DCGM_INT32_BLANK'`. Fix: - Add a single `_power_limits(gpu_idx)` helper that calls `DcgmSystem.discovery.GetGpuAttributes(gpu_id).powerLimits` - the synchronous device-info API that wraps `dcgmGetDeviceAttributes`. One RPC instead of four, returns a `c_dcgmDevicePowerLimits_v1` with integer-watt fields populated from the hostengine's discovery state, no field-cache dependency. - `constraints_w`, `current_w`, `default_w`, `restore_default` all route through this helper. - `get_uuid` and `_ensure_identity_map._read_dcgm_uuids` now use `GetGpuAttributes(gpu_id).identifiers.uuid`. - `apply_cap` imports `dcgmvalue` and swaps all seven `DCGM_INT32_BLANK` references onto it. Test infrastructure overhaul (`test_dcgm_actuator.py`): - `_make_dcgm_modules` adds a `dcgmvalue` MagicMock with all four blank sentinels; removes `DCGM_INT32_BLANK` from `dcgm_structs` mock. - `_make_gpu_attrs` extended to carry both `.identifiers.uuid` and `.powerLimits.{cur,default,enforced,min,max}PowerLimit`. - `_wire_handle` wires `GetGpuAttributes(gid)` per-gpu_id. - `_seed_constraints_and_uuid` consolidated onto the unified GetGpuAttributes path. - Five `modules["dcgm_agent"].dcgmEntityGetLatestValues.assert_not_called()` regression guards added across the suite - locks in "no more silent field-cache reads of static device info." E2e re-validation: with this fix in place, the same 8xA100 rig reports `PASS: NvmlActuator and DcgmActuator agree on all probes (tolerance +/- 2.0 W)`, exit code 0, with nvidia-smi confirming `apply_cap(250)` -> 250 W and `restore_default()` -> 400 W on every GPU. Doc: `docs/design-docs/power-agent-dual-actuator.md` v1.10 changelog row added; §6.1 / §6.3 / §7 / §12 / §13 patched to match (Protocol-level docstrings for `current_w`/`default_w`, `get_uuid` / `_power_limits` / `restore_default` code samples, `_apply_cap_inner` import + blank-constant sample, attribution table row 2, summary clause, reference-material entries for `pydcgm/dcgmvalue.py` and `GetGpuAttributes`). Local gates: 163/163 power-agent unit tests pass; pre-commit isort/black/flake8/codespell/ruff/EOF/whitespace clean on the three changed files. Broader `tests/` + `pytest-marker-report` blocked by the known Windows-only `fcntl` import in `tests/conftest.py:22`; CI runs those on Linux. Signed-off-by: Kai Ma <kaim@nvidia.com>
Three Dockerfile bugs combined to make the DCGM-mode image unbuildable on a fresh checkout. Fixing any one in isolation leaves the build broken, so they travel together: 1. DCGM_IMAGE default 'nvcr.io/nvidia/cloud-native/dcgm:4.2.3-2-ubuntu22.04' does not exist on NGC (verified 2026-05-21 via 'docker manifest inspect' → 404). Bump to 4.5.1-1-ubuntu22.04, the only resolvable 4.x tag. 2. DCGM 4.5+ relocated python bindings from /usr/local/dcgm/bindings/python3/ to /usr/share/datacenter-gpu-manager-4/bindings/python3/. The previous COPY would silently copy zero files under the new pin. Switch the source path to the 4.5+ location. 3. NGC's DCGM 4.5+ runtime image ships pydcgm with DcgmGroup.py:20 doing 'import logger' — but logger.py lives in DCGM's source tree under testing/python3/ and is NOT packaged. Without a shim every DcgmGroup construction raises ModuleNotFoundError. Add a 10-line stdlib-logging adapter at components/power_agent/logger.py and COPY it into /opt/dcgm/python/logger.py during the runtime stage. This unblocks 'docker build -f components/power_agent/Dockerfile' on a fresh clone (verified locally via 'docker buildx build --build-arg DCGM_IMAGE=...4.5.1-1-ubuntu22.04' against viking-prod-216 on 2026-05-21, image pushed to ttl.sh/dynamo-pa-kaim-dcgm45-v2:24h and used by the Path-B live test on aks-a100b-22138447-vmss000000). Refs: PR #9790 review, Power Agent live-test findings #1/#2/#6. Signed-off-by: Kai Ma <kaim@nvidia.com>
components/power_agent/actuator.py:807 uses dcgm_structs.c_dcgmDeviceConfig_v2,
a struct that only exists in DCGM 4.x. With 3.x bindings the agent's
DcgmActuator.init() succeeded, opened the hostengine connection, and
ran NVML init cleanly — then crashed mid-first-reconcile with
'AttributeError: module dcgm_structs has no attribute
c_dcgmDeviceConfig_v2' after some GPUs were already capped. The
SIGTERM-restore path won't run when the actuator never finished
registering with _active_actuator, so the GPUs are left at custom caps.
Add a 7-line hasattr check immediately after the dcgm_structs import
in init() that raises a RuntimeError with:
- the missing struct name (so the error is grep-able)
- the required DCGM major (>=4.0)
- the DCGM_IMAGE build-arg + canonical 4.5.1 tag (so operators
know exactly what to bump)
Guards against accidental Dockerfile regressions to a 3.x base image.
Pair with the Dockerfile fix in 4820ca7 (which bumps the default
to 4.5.1-1-ubuntu22.04) so the default build path is consistent with
the runtime contract.
Unit test covers both happy path (DCGM 4.x mock) and 3.x rejection
(MagicMock with c_dcgmDeviceConfig_v2 deleted), asserting init fails
BEFORE the first pydcgm.DcgmHandle call so misconfigured deployments
don't half-init and leave hostengine sockets dangling.
Refs: PR #9790 review, Power Agent live-test finding #5.
Signed-off-by: Kai Ma <kaim@nvidia.com>
0eda1d7 to
1e1ef6f
Compare
… log, pod-UID dedup, argparse) Folds the four CodeRabbit findings on the foundation PR back into this branch so each fix lives with the code it changes, rather than leaking into the downstream DCGM-actuator PR (#9790): * _load_previously_managed_gpus: catch OSError (not just FileNotFoundError) and validate that the JSON root is a dict and managed_uuids is a list. Malformed state files now log a warning and return an empty set instead of crashing the agent at startup. * _handle_sigterm: replace 'except Exception: pass' on pynvml.nvmlShutdown() with logger.exception so shutdown-time NVML faults appear in pod logs. We still fall through to _shutdown.set() so SIGTERM never hangs the container. * _reconcile_gpu: dedup the (pod_uid, annotation) list by UID before applying multi-pod policy. A single pod with N PIDs on one GPU was being counted as N pods, falsely tripping the multi-pod-conflict branch and the multi_pod_gpu_total metric. * main(): move 'import argparse' to module scope per the project's import-placement convention. Regression coverage: existing components/power_agent/tests/ suite (43 tests) still passes locally; behavior-specific tests for these four fixes already live on PR #9790 and remain there. Signed-off-by: Kai Ma <kaim@nvidia.com>
… log, pod-UID dedup, argparse) Folds the four CodeRabbit findings on the foundation PR back into this branch so each fix lives with the code it changes, rather than leaking into the downstream DCGM-actuator PR (#9790): * _load_previously_managed_gpus: catch OSError (not just FileNotFoundError) and validate that the JSON root is a dict and managed_uuids is a list. Malformed state files now log a warning and return an empty set instead of crashing the agent at startup. * _handle_sigterm: replace 'except Exception: pass' on pynvml.nvmlShutdown() with logger.exception so shutdown-time NVML faults appear in pod logs. We still fall through to _shutdown.set() so SIGTERM never hangs the container. * _reconcile_gpu: dedup the (pod_uid, annotation) list by UID before applying multi-pod policy. A single pod with N PIDs on one GPU was being counted as N pods, falsely tripping the multi-pod-conflict branch and the multi_pod_gpu_total metric. * main(): move 'import argparse' to module scope per the project's import-placement convention. Regression coverage: existing components/power_agent/tests/ suite (43 tests) still passes locally; behavior-specific tests for these four fixes already live on PR #9790 and remain there. Signed-off-by: Kai Ma <kaim@nvidia.com>
1e1ef6f to
29f0831
Compare
Aligns docs/design-docs/power-agent-helm-chart-plan.md with the v1.1.0 chart that PR #9790 ships. The plan was authored for the NVML-only v1.0.0 chart in PR #9682 and went stale once PR #9790 layered agent.actuator + agent.dcgm.* + validateActuator + validateEnforce + the helm-unittest suite on top. Two changelog rows capture the refresh: v1.3 - first pass: 8 reviewer findings closed (3 blocking, 4 major, 2 medium, 1 low) on Status header, values surface, dev ConfigMap recipe, image-tag pinning, helm-unittest gating, internal-dev-doc references, daemonset name, file/LOC accounting. v1.4 - second pass: 5 follow-up findings closed (2 major, 2 medium, 1 low) on Status self-contradiction, stale section 4.2 dev-block comment, overstated helm-unittest coverage prose, unrunnable section 5.4 positive helm template overlays (missing --set image.tag), and the lingering filename reference in the v1.3 changelog. No design reversal: every section 6 decision and the chart shape are unchanged. Only the values surface (extended), helper set (extended with two template-time validators), and validation-gate list (helm-unittest now required) grew. Every claim in the refreshed doc was verified against on-disk state: power_agent.py:706-804 for the 8-flag CLI surface, values.yaml for the dev-block recipe, _helpers.tpl for the five-helper set, and the two tests/validate_*_test.yaml files for the 24 enumerated unittest cases. Signed-off-by: Kai Ma <kaim@nvidia.com>
Aligns docs/design-docs/power-agent-helm-chart-plan.md with the v1.2.0 chart that the preceding commit ships. The plan was authored for the v1.0.0 NVML-only chart in PR #9682, refreshed to v1.1.0 in the PR #9790 dual-actuator work, and went stale again once v1.2.0 landed image.digest + the canonical-OCI imageRef helper on top. Four reviewer findings closed (v1.5 changelog row covers all four): (1) Truncated SHA-256 digests slipped through the v1.1.0 helper - already fixed in the preceding chart commit; this commit propagates the rule into the §4.4 helper snippet and the §5.4 validation gates. (2) Whitespace-padded tags rendered raw image references - same treatment: §4.4 documents the trim-then-reject contract. (3) §4.3 / §4.4 / §5.4 stale on image.digest: - §4.3 CodeRabbit-comment row rewritten to describe both helpers (validateImageTag + imageRef), both fields (tag + digest), the canonical repo@digest form, and the PR9682 follow-up that added the separate field. - §4.4 helper snippet replaced with the actual v1.2.0 validator (full rule set + per-rule rationale comments) plus the new imageRef helper. - §5.4 expected helm-unittest output bumped 24 -> 46 passed with a one-line breakdown of the +22 new cases. (4) §4.1 / §4.2 stale on file count + helper list + values surface: - §4.1 said 13 files (omitted .helmignore and the new validate_image_tag_test.yaml). Corrected to 14 files (7 templates + 3 helm-unittests + 4 root files). The _helpers.tpl helper-list line gained imageRef and chart entries. The daemonset.yaml annotation now mentions it routes through imageRef. LOC estimate bumped ~1,430 -> ~1,700 with a breakdown of what v1.2.0 added on top of v1.0.0 / v1.1.0. - §4.2 values snippet had only image.tag with no image.digest. Added the field with the same per-rule comment block that ships in values.yaml (OCI form, 64-hex requirement, mutex with image.tag, PR9682 rationale). Status header bumped to chart v1.2.0 with a one-line v1.2.0 rationale (additive opt-in field, no breaking change to existing tag-pinned installs). Revision-history table gains the v1.5 entry summarising all four findings; older v1.1 - v1.4 entries untouched. No design reversal: every §6 decision and the chart shape are unchanged. Only the values surface (one additive field), helper set (two helpers: imageRef new, validateImageTag rewritten), and helm-unittest count (24 -> 46) grew. Every claim in the refreshed doc was verified against on-disk state: Chart.yaml for the version bump, values.yaml for the image.digest field comment, _helpers.tpl for both helpers, tests/validate_image_tag_test.yaml for the 22 enumerated cases (verified via `helm unittest`). Signed-off-by: Kai Ma <kaim@nvidia.com>
99138c4 to
abcc922
Compare
…ttributes (v1.10 e2e fix) First in-cluster DCGM parity run on an 8xA100 SXM node (PR #9790 e2e) surfaced three production defects in DcgmActuator, all rooted in pydcgm API misuse that the mocked unit suite couldn't catch: 1. `get_uuid` and `_ensure_identity_map._read_dcgm_uuids` read `DCGM_FI_DEV_UUID` via `dcgmEntityGetLatestValues`. That API returns the field cache; on a fresh nv-hostengine 4.5.3 with no companion watcher subscribed via `dcgmWatchFields`, the cache returns `DCGM_STR_BLANK = "<<<NULL>>>"`. The identity map then raised "8 GPU UUID(s) visible to DCGM are not visible to NVML" on every cluster startup. 2. `constraints_w` / `current_w` / `default_w` read the four `DCGM_FI_DEV_POWER_MGMT_LIMIT{,_MIN,_MAX,_DEF}` fields the same way. Every read returned `DCGM_FP64_BLANK = 2^47`, so the clamp in `apply_cap` escalated a requested 250 W up to the blank max and tried to write 140737488355328 W. 3. The workload-power-profile blanking loop in `_apply_cap_inner` reached for `dcgm_structs.DCGM_INT32_BLANK`. The constant actually lives in `dcgmvalue` (`/shared/pydcgm/dcgmvalue.py:17` in the DCGM 4.5.3 apt bindings); pre-v1.10 the first cap write raised `AttributeError: module 'dcgm_structs' has no attribute 'DCGM_INT32_BLANK'`. Fix: - Add a single `_power_limits(gpu_idx)` helper that calls `DcgmSystem.discovery.GetGpuAttributes(gpu_id).powerLimits` - the synchronous device-info API that wraps `dcgmGetDeviceAttributes`. One RPC instead of four, returns a `c_dcgmDevicePowerLimits_v1` with integer-watt fields populated from the hostengine's discovery state, no field-cache dependency. - `constraints_w`, `current_w`, `default_w`, `restore_default` all route through this helper. - `get_uuid` and `_ensure_identity_map._read_dcgm_uuids` now use `GetGpuAttributes(gpu_id).identifiers.uuid`. - `apply_cap` imports `dcgmvalue` and swaps all seven `DCGM_INT32_BLANK` references onto it. Test infrastructure overhaul (`test_dcgm_actuator.py`): - `_make_dcgm_modules` adds a `dcgmvalue` MagicMock with all four blank sentinels; removes `DCGM_INT32_BLANK` from `dcgm_structs` mock. - `_make_gpu_attrs` extended to carry both `.identifiers.uuid` and `.powerLimits.{cur,default,enforced,min,max}PowerLimit`. - `_wire_handle` wires `GetGpuAttributes(gid)` per-gpu_id. - `_seed_constraints_and_uuid` consolidated onto the unified GetGpuAttributes path. - Five `modules["dcgm_agent"].dcgmEntityGetLatestValues.assert_not_called()` regression guards added across the suite - locks in "no more silent field-cache reads of static device info." E2e re-validation: with this fix in place, the same 8xA100 rig reports `PASS: NvmlActuator and DcgmActuator agree on all probes (tolerance +/- 2.0 W)`, exit code 0, with nvidia-smi confirming `apply_cap(250)` -> 250 W and `restore_default()` -> 400 W on every GPU. Doc: `docs/design-docs/power-agent-dual-actuator.md` v1.10 changelog row added; §6.1 / §6.3 / §7 / §12 / §13 patched to match (Protocol-level docstrings for `current_w`/`default_w`, `get_uuid` / `_power_limits` / `restore_default` code samples, `_apply_cap_inner` import + blank-constant sample, attribution table row 2, summary clause, reference-material entries for `pydcgm/dcgmvalue.py` and `GetGpuAttributes`). Local gates: 163/163 power-agent unit tests pass; pre-commit isort/black/flake8/codespell/ruff/EOF/whitespace clean on the three changed files. Broader `tests/` + `pytest-marker-report` blocked by the known Windows-only `fcntl` import in `tests/conftest.py:22`; CI runs those on Linux. Signed-off-by: Kai Ma <kaim@nvidia.com>
Three Dockerfile bugs combined to make the DCGM-mode image unbuildable on a fresh checkout. Fixing any one in isolation leaves the build broken, so they travel together: 1. DCGM_IMAGE default 'nvcr.io/nvidia/cloud-native/dcgm:4.2.3-2-ubuntu22.04' does not exist on NGC (verified 2026-05-21 via 'docker manifest inspect' → 404). Bump to 4.5.1-1-ubuntu22.04, the only resolvable 4.x tag. 2. DCGM 4.5+ relocated python bindings from /usr/local/dcgm/bindings/python3/ to /usr/share/datacenter-gpu-manager-4/bindings/python3/. The previous COPY would silently copy zero files under the new pin. Switch the source path to the 4.5+ location. 3. NGC's DCGM 4.5+ runtime image ships pydcgm with DcgmGroup.py:20 doing 'import logger' — but logger.py lives in DCGM's source tree under testing/python3/ and is NOT packaged. Without a shim every DcgmGroup construction raises ModuleNotFoundError. Add a 10-line stdlib-logging adapter at components/power_agent/logger.py and COPY it into /opt/dcgm/python/logger.py during the runtime stage. This unblocks 'docker build -f components/power_agent/Dockerfile' on a fresh clone (verified locally via 'docker buildx build --build-arg DCGM_IMAGE=...4.5.1-1-ubuntu22.04' against viking-prod-216 on 2026-05-21, image pushed to ttl.sh/dynamo-pa-kaim-dcgm45-v2:24h and used by the Path-B live test on aks-a100b-22138447-vmss000000). Refs: PR #9790 review, Power Agent live-test findings #1/#2/#6. Signed-off-by: Kai Ma <kaim@nvidia.com>
components/power_agent/actuator.py:807 uses dcgm_structs.c_dcgmDeviceConfig_v2,
a struct that only exists in DCGM 4.x. With 3.x bindings the agent's
DcgmActuator.init() succeeded, opened the hostengine connection, and
ran NVML init cleanly — then crashed mid-first-reconcile with
'AttributeError: module dcgm_structs has no attribute
c_dcgmDeviceConfig_v2' after some GPUs were already capped. The
SIGTERM-restore path won't run when the actuator never finished
registering with _active_actuator, so the GPUs are left at custom caps.
Add a 7-line hasattr check immediately after the dcgm_structs import
in init() that raises a RuntimeError with:
- the missing struct name (so the error is grep-able)
- the required DCGM major (>=4.0)
- the DCGM_IMAGE build-arg + canonical 4.5.1 tag (so operators
know exactly what to bump)
Guards against accidental Dockerfile regressions to a 3.x base image.
Pair with the Dockerfile fix in 4820ca7 (which bumps the default
to 4.5.1-1-ubuntu22.04) so the default build path is consistent with
the runtime contract.
Unit test covers both happy path (DCGM 4.x mock) and 3.x rejection
(MagicMock with c_dcgmDeviceConfig_v2 deleted), asserting init fails
BEFORE the first pydcgm.DcgmHandle call so misconfigured deployments
don't half-init and leave hostengine sockets dangling.
Refs: PR #9790 review, Power Agent live-test finding #5.
Signed-off-by: Kai Ma <kaim@nvidia.com>
|
Rebased onto the refreshed What moved:
CI rerunning now. @coderabbitai review |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
components/power_agent/tests/test_managed_state_parser.py (1)
85-275:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd required pytest markers (scheduling + GPU + type) to this test module.
The newly added tests are unmarked and don’t satisfy the required test marker policy.
As per coding guidelines: “Every test must have at least one scheduling marker… at least one GPU marker… and at least one type marker…”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_managed_state_parser.py` around lines 85 - 275, Import pytest at the top of the test module and add a module-level pytestmark list that applies the required markers (scheduling + GPU + type) to all tests, e.g. place `import pytest` and `pytestmark = [pytest.mark.scheduling, pytest.mark.gpu, pytest.mark.unit]` (or whichever type marker is appropriate) near the top so TestHappyPath, TestMissingFile, TestOsErrorSiblings, TestMalformedJson, TestNonObjectRoot, TestManagedUuidsNotAList, and TestEntryTypeValidation all inherit the markers.components/power_agent/tests/test_orphan_recovery.py (1)
85-286:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd required pytest markers (scheduling + GPU + type) to this test module.
This suite needs the mandatory test markers before merge.
As per coding guidelines: “Every test must have at least one scheduling marker… at least one GPU marker… and at least one type marker…”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_orphan_recovery.py` around lines 85 - 286, The module lacks required pytest markers (scheduling + GPU + type); add a module-level pytestmark so every test in this file gets those markers: import pytest at top of the file and define pytestmark = [pytest.mark.<scheduling_marker>, pytest.mark.<gpu_marker>, pytest.mark.<type_marker>] (replace placeholders with the project's chosen markers), which will apply to the test classes such as TestUuidGating, TestWorkloadBusySkip, TestCurrentVsDefaultGuard, TestPerGpuExceptionIsolation, and TestManagedSetPruning.components/power_agent/tests/test_shutdown.py (1)
46-229:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd required pytest markers (scheduling + GPU + type) to this test module.
This file currently lacks the mandatory test classification markers.
As per coding guidelines: “Every test must have at least one scheduling marker… at least one GPU marker… and at least one type marker…”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_shutdown.py` around lines 46 - 229, Add the required pytest classification markers by defining a module-level pytestmark list (e.g. pytestmark = [pytest.mark.scheduling, pytest.mark.gpu, pytest.mark.type]) so every test in this module (including TestSigtermViaActuator and TestSigtermFallback and their methods like _handle_sigterm interactions) carries scheduling, GPU and type markers; place the pytestmark definition near the top of the file (above the test classes) and import pytest if not already imported.components/power_agent/tests/test_reconcile_wiring.py (1)
46-326:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd required pytest markers (scheduling + GPU + type) for this module.
These tests currently have none of the required markers.
Suggested patch
+import pytest + +pytestmark = [ + pytest.mark.post_merge, # or pre_merge/nightly/etc. per intended cadence + pytest.mark.gpu_0, + pytest.mark.unit, +]As per coding guidelines: “Every test must have at least one scheduling marker… at least one GPU marker… and at least one type marker…”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_reconcile_wiring.py` around lines 46 - 326, Add the required pytest markers by importing pytest and applying scheduling, GPU, and type markers to this test module; either set a module-level pytestmark list or add decorators to each test class (e.g. TestReconcileGpuRoutesViaActuator, TestReconcileGpuDedupesByPodUid, TestReconcileGpuPolicyResolution) to include at least one scheduling marker, one GPU marker, and one type marker per the guidelines so all tests in this file are properly marked.
🧹 Nitpick comments (4)
deploy/helm/charts/power-agent/tests/validate_enforce_test.yaml (1)
54-95: ⚡ Quick winStrengthen enforce-flag assertion coverage for accepted inputs and nvml dead-config checks.
Line 54 onward accepts multiple string forms with only
notFailedTemplate, and Line 136 claims “ANY allowlisted value” absence while checking only a subset. Add exactcontains/notContainsassertions for the remaining allowlisted renderable forms to catch silent drift.Proposed test hardening pattern
- it: should accept enforce=1 (string) set: agent.dcgm.enforce: "1" asserts: - notFailedTemplate: {} + - contains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=1 - it: should accept enforce=0 (string) set: agent.dcgm.enforce: "0" asserts: - notFailedTemplate: {} + - contains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=0 - it: should accept enforce=yes set: agent.dcgm.enforce: "yes" asserts: - notFailedTemplate: {} + - contains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=yes- notContains: path: spec.template.spec.containers[0].command content: --dcgm-enforce=false + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=1 + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=0 + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=yes + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=no + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=on + - notContains: + path: spec.template.spec.containers[0].command + content: --dcgm-enforce=offAlso applies to: 136-147
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@deploy/helm/charts/power-agent/tests/validate_enforce_test.yaml` around lines 54 - 95, The tests under validate_enforce_test.yaml currently only use notFailedTemplate for many agent.dcgm.enforce string variants; update each case (e.g., the tests setting agent.dcgm.enforce to "1","0","yes","no","on","off","TRUE") to also assert the rendered templates explicitly include or exclude the expected configuration snippets by adding contains/notContains assertions for the corresponding normalized values (e.g., numeric 1/0, true/false, on/off forms) so silent drift is caught, and apply the same hardening to the other block referenced (the ANY allowlisted values section around lines 136-147) to cover all allowed renderable forms and the nvml dead-config checks rather than relying solely on notFailedTemplate.components/power_agent/tests/test_reconcile_wiring.py (1)
218-219: ⚡ Quick winMove function-scope imports to module scope.
from tests.test_multi_pod_policy import _FakeMetricsis imported inside test methods; this should be hoisted to top-level imports.Suggested patch
import unittest from unittest.mock import MagicMock, patch import power_agent from power_agent import PowerAgent +from tests.test_multi_pod_policy import _FakeMetrics @@ - from tests.test_multi_pod_policy import _FakeMetrics - agent.metrics = _FakeMetrics() @@ - from tests.test_multi_pod_policy import _FakeMetrics - agent.metrics = _FakeMetrics() @@ - from tests.test_multi_pod_policy import _FakeMetrics - agent.metrics = _FakeMetrics()As per coding guidelines: “ensure imports are only at module scope (no imports inside functions/classes)”.
Also applies to: 243-244, 269-270
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_reconcile_wiring.py` around lines 218 - 219, The test module imports `_FakeMetrics` inside test functions; hoist `from tests.test_multi_pod_policy import _FakeMetrics` to the top-level imports in the module (module scope) and remove the function-scope imports in the test methods (including the other occurrences where `_FakeMetrics` is imported inside tests). Ensure the module-level import is added near the other test imports so all tests reference the same top-level `_FakeMetrics`.components/power_agent/tests/test_managed_state_parser.py (1)
60-67: ⚡ Quick winHoist
loggingimports to module scope.
import loggingis currently inside methods; move it to top-level imports for consistency with Python guidelines.As per coding guidelines: “ensure imports are only at module scope (no imports inside functions/classes)”.
Also applies to: 114-121
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/test_managed_state_parser.py` around lines 60 - 67, Move the "import logging" statements out of the test functions and into module-level imports in test_managed_state_parser.py; remove the in-function imports at the locations that set up captured, handler, and logger (the blocks that create captured: list[logging.LogRecord], handler = logging.Handler(), handler.emit = captured.append, handler.setLevel(logging.WARNING), and logger = logging.getLogger("power_agent")), so the tests use the top-level logging import consistently (also apply the same change to the other block referenced in the file).components/power_agent/tests/e2e_actuator_parity.py (1)
497-500: ⚡ Quick winAvoid silently swallowing NVML shutdown errors.
This
except Exception: passhides teardown faults and makes parity-run failures hard to diagnose.Suggested patch
finally: try: pynvml.nvmlShutdown() - except Exception: - pass + except Exception as e: + print(f"WARNING: pynvml.nvmlShutdown() failed: {e}", file=sys.stderr)As per coding guidelines: “fail fast (don’t swallow exceptions, catch specific exceptions only, and if catching
Exceptionthen log and re-raise)”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/power_agent/tests/e2e_actuator_parity.py` around lines 497 - 500, The try/except that currently calls pynvml.nvmlShutdown() and swallows all exceptions should be changed to catch the specific NVML exception type (pynvml.NVMLError) instead of Exception, log the error with traceback, and re-raise so teardown failures are visible; update the block around pynvml.nvmlShutdown() (replace the bare except Exception: pass) to except pynvml.NVMLError as e: logger.exception("pynvml.nvmlShutdown failed: %s", e) and raise, and if a module-level logger isn’t present add logger = logging.getLogger(__name__) and import logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/power_agent/actuator.py`:
- Around line 775-785: The except-block around the call to self._apply_cap_inner
should only catch DCGM write-related exceptions instead of all Exception;
replace "except Exception as e:" with a narrow catch such as "except DcgmError
as e" (or the specific DCGM client exception types your project uses, e.g.,
DcgmApiError / DcgmWriteError) and ensure those exception classes are imported,
keep the existing logger.error and metrics.inc for that case, and re-raise any
other unexpected exceptions so programming/runtime bugs are not swallowed by
apply_cap/_apply_cap_inner.
- Around line 539-540: The except block that swallows all exceptions after
calling _handle.Shutdown() must be changed to avoid hiding failures: catch only
expected exceptions or catch Exception but log the exception context before
continuing. Locate the try/except surrounding _handle.Shutdown() in actuator.py
(the block that currently reads "except Exception: pass") and replace the silent
swallow with a logged error using the module logger (include the exception
instance and stacktrace) or re-raise critical exceptions after logging; ensure
the log message mentions "_handle.Shutdown" so it’s easy to find in logs.
- Around line 618-622: The RuntimeError raised in DcgmActuator.list_running_pids
when a GPU UUID is no longer visible should preserve the original KeyError as
the root cause; change the raise so it uses exception chaining (raise
RuntimeError(...) from err) where err is the caught KeyError from the UUID-map
lookup, keeping the message unchanged but appending "from err" to preserve the
original traceback and context for uuid and gpu_idx.
In `@components/power_agent/power_agent.py`:
- Around line 169-177: The current check uses valid = {u for u in uuids if
isinstance(u, str)} which turns uuids into a set and conflates duplicates with
non-string entries; replace this by collecting only string entries without
deduping (e.g., valid_list = [u for u in uuids if isinstance(u, str)]) and
compute non_string_count = sum(1 for u in uuids if not isinstance(u, str)); then
change the logger.warning invocation (the one referencing _MANAGED_STATE_PATH)
to report non_string_count and the number of kept UUIDs (use len(valid_list) or
len(set(valid_list)) depending on whether you intend to report raw entries or
unique UUIDs) instead of comparing len(valid) to len(uuids).
In `@deploy/helm/charts/power-agent/README.md`:
- Around line 49-53: The README currently references a specific chart version
("chart v1.1.0 requires the v1.1.0 image"); update the wording to tie the
requirement to the chart's appVersion instead of a hard-coded chart version:
state that the image tag MUST match the chart's `appVersion`, and explain that
this is necessary because the chart renders the `--actuator` / `--dcgm-*` CLI
flags and `power_agent.py` imports the appVersion-specific `actuator` module;
replace the version-specific sentence with this appVersion-based guidance so
future chart bumps don't require text edits.
In `@deploy/helm/charts/power-agent/templates/_helpers.tpl`:
- Around line 117-119: The helper currently only checks for leading/trailing
whitespace on $tag but allows internal whitespace (e.g., "v1 .1.0") which
produces invalid OCI image references; update the validation in the template
that handles image.tag (the block using $tag and the fail message) to reject any
whitespace characters anywhere in $tag (not just trim differences) by testing
for matches against a whitespace pattern and calling fail with the existing
informative message when any whitespace is found; keep the same fail text and
hint but trigger it when $tag contains internal spaces or other whitespace.
In `@docs/design-docs/power-agent-helm-chart-plan.md`:
- Line 828: The Markdown has unlabeled fenced code blocks that trigger MD040;
update each triple-backtick block in power-agent-helm-chart-plan.md to include a
language identifier (e.g., change ``` to ```text or to a specific language such
as ```yaml, ```bash, or ```json as appropriate) so the linter recognizes the
block language; apply the same change to the other unlabeled fences mentioned in
the comment to resolve MD040.
- Line 29: The heading "### Revision history" is skipping a level; change that
heading to "## Revision history" (or add a parent "##" section before it) so the
document preserves correct heading hierarchy and satisfies MD001; update the
line containing "### Revision history" in
docs/design-docs/power-agent-helm-chart-plan.md accordingly.
- Around line 826-846: Update the stale v1.1.0 summary counts in sections §5.2
and §8 to match the v1.2.0 source-of-truth numbers used elsewhere in this
document: replace "13 files / 2 helm-unittests / 24 passed" with "14 files / 3
helm-unittests / 46 passed" (and any adjacent totals like "Chart-only total" or
LOC if they differ), and make the identical corrections where the same summary
appears around lines 1151-1160 so all references are consistent.
---
Outside diff comments:
In `@components/power_agent/tests/test_managed_state_parser.py`:
- Around line 85-275: Import pytest at the top of the test module and add a
module-level pytestmark list that applies the required markers (scheduling + GPU
+ type) to all tests, e.g. place `import pytest` and `pytestmark =
[pytest.mark.scheduling, pytest.mark.gpu, pytest.mark.unit]` (or whichever type
marker is appropriate) near the top so TestHappyPath, TestMissingFile,
TestOsErrorSiblings, TestMalformedJson, TestNonObjectRoot,
TestManagedUuidsNotAList, and TestEntryTypeValidation all inherit the markers.
In `@components/power_agent/tests/test_orphan_recovery.py`:
- Around line 85-286: The module lacks required pytest markers (scheduling + GPU
+ type); add a module-level pytestmark so every test in this file gets those
markers: import pytest at top of the file and define pytestmark =
[pytest.mark.<scheduling_marker>, pytest.mark.<gpu_marker>,
pytest.mark.<type_marker>] (replace placeholders with the project's chosen
markers), which will apply to the test classes such as TestUuidGating,
TestWorkloadBusySkip, TestCurrentVsDefaultGuard, TestPerGpuExceptionIsolation,
and TestManagedSetPruning.
In `@components/power_agent/tests/test_reconcile_wiring.py`:
- Around line 46-326: Add the required pytest markers by importing pytest and
applying scheduling, GPU, and type markers to this test module; either set a
module-level pytestmark list or add decorators to each test class (e.g.
TestReconcileGpuRoutesViaActuator, TestReconcileGpuDedupesByPodUid,
TestReconcileGpuPolicyResolution) to include at least one scheduling marker, one
GPU marker, and one type marker per the guidelines so all tests in this file are
properly marked.
In `@components/power_agent/tests/test_shutdown.py`:
- Around line 46-229: Add the required pytest classification markers by defining
a module-level pytestmark list (e.g. pytestmark = [pytest.mark.scheduling,
pytest.mark.gpu, pytest.mark.type]) so every test in this module (including
TestSigtermViaActuator and TestSigtermFallback and their methods like
_handle_sigterm interactions) carries scheduling, GPU and type markers; place
the pytestmark definition near the top of the file (above the test classes) and
import pytest if not already imported.
---
Nitpick comments:
In `@components/power_agent/tests/e2e_actuator_parity.py`:
- Around line 497-500: The try/except that currently calls pynvml.nvmlShutdown()
and swallows all exceptions should be changed to catch the specific NVML
exception type (pynvml.NVMLError) instead of Exception, log the error with
traceback, and re-raise so teardown failures are visible; update the block
around pynvml.nvmlShutdown() (replace the bare except Exception: pass) to except
pynvml.NVMLError as e: logger.exception("pynvml.nvmlShutdown failed: %s", e) and
raise, and if a module-level logger isn’t present add logger =
logging.getLogger(__name__) and import logging.
In `@components/power_agent/tests/test_managed_state_parser.py`:
- Around line 60-67: Move the "import logging" statements out of the test
functions and into module-level imports in test_managed_state_parser.py; remove
the in-function imports at the locations that set up captured, handler, and
logger (the blocks that create captured: list[logging.LogRecord], handler =
logging.Handler(), handler.emit = captured.append,
handler.setLevel(logging.WARNING), and logger =
logging.getLogger("power_agent")), so the tests use the top-level logging import
consistently (also apply the same change to the other block referenced in the
file).
In `@components/power_agent/tests/test_reconcile_wiring.py`:
- Around line 218-219: The test module imports `_FakeMetrics` inside test
functions; hoist `from tests.test_multi_pod_policy import _FakeMetrics` to the
top-level imports in the module (module scope) and remove the function-scope
imports in the test methods (including the other occurrences where
`_FakeMetrics` is imported inside tests). Ensure the module-level import is
added near the other test imports so all tests reference the same top-level
`_FakeMetrics`.
In `@deploy/helm/charts/power-agent/tests/validate_enforce_test.yaml`:
- Around line 54-95: The tests under validate_enforce_test.yaml currently only
use notFailedTemplate for many agent.dcgm.enforce string variants; update each
case (e.g., the tests setting agent.dcgm.enforce to
"1","0","yes","no","on","off","TRUE") to also assert the rendered templates
explicitly include or exclude the expected configuration snippets by adding
contains/notContains assertions for the corresponding normalized values (e.g.,
numeric 1/0, true/false, on/off forms) so silent drift is caught, and apply the
same hardening to the other block referenced (the ANY allowlisted values section
around lines 136-147) to cover all allowed renderable forms and the nvml
dead-config checks rather than relying solely on notFailedTemplate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4b586c90-dca2-4d5f-aaae-4f35d04c75ef
📒 Files selected for processing (26)
components/power_agent/Dockerfilecomponents/power_agent/README.mdcomponents/power_agent/actuator.pycomponents/power_agent/logger.pycomponents/power_agent/power_agent.pycomponents/power_agent/tests/e2e_actuator_parity.pycomponents/power_agent/tests/test_actuator_protocol.pycomponents/power_agent/tests/test_actuator_selection.pycomponents/power_agent/tests/test_dcgm_actuator.pycomponents/power_agent/tests/test_managed_state_parser.pycomponents/power_agent/tests/test_multi_pod_policy.pycomponents/power_agent/tests/test_orphan_recovery.pycomponents/power_agent/tests/test_reconcile_wiring.pycomponents/power_agent/tests/test_shutdown.pydeploy/helm/charts/power-agent/Chart.yamldeploy/helm/charts/power-agent/README.mddeploy/helm/charts/power-agent/templates/NOTES.txtdeploy/helm/charts/power-agent/templates/_helpers.tpldeploy/helm/charts/power-agent/templates/daemonset.yamldeploy/helm/charts/power-agent/templates/dev-pod.yamldeploy/helm/charts/power-agent/tests/validate_actuator_test.yamldeploy/helm/charts/power-agent/tests/validate_enforce_test.yamldeploy/helm/charts/power-agent/tests/validate_image_tag_test.yamldeploy/helm/charts/power-agent/values.yamldocs/design-docs/power-agent-dual-actuator.mddocs/design-docs/power-agent-helm-chart-plan.md
|
/ok to test 7226298 |
|
/ok to test 3b84f22 |
|
/ok to test 85df904 |
|
@nv-anants @sttts (cc @dynamo-docs) — re-review request. Since the last review pass I pushed one commit,
A real-DCGM ABI/load smoke test is tracked as a follow-up in #11213. All 20 review threads are resolved, and full CI is re-running on |
|
@nv-anants @sttts - one small clarification to keep the review record precise, then the merge ask. Two older resolved-thread replies were a little too broad compared with the current code. Device-count refresh: the frozen startup count bug is fixed because reconcile_once refreshes self._actuator.device_count() each cycle, so topology growth is picked up and shrink is not carried forward from the stale startup count. My earlier 'a shrink no longer raises per-index errors' sentence was too strong: if DCGM reconnects/re-enumerates during a GPU loop after range(self.device_count) has already been captured, that cycle can still see transient per-index errors; the next cycle refreshes and recovers. Restore outcome / ownership retirement: current contract is True = restored a live below-default cap, None = no write needed / cleanly absent / already at default, False = inconclusive/retain ownership. Current callers retire/prune on True or None, and retain only on False. Some older wording said only True prunes and already-at-default remains tracked; that was stale. The enum cleanup remains tracked separately in #11193 as agreed. I did another pass over the code/comments after the final review round. The code changes look consistent with the replies, CI is green on head 85df904, and the PR is only blocked on review approval. Could you approve when you get a chance if this looks good now? Thanks. |
|
Codex xhigh review: Findings[P1]
|
|
And a Codex smell check: Code smell ranking
|
|
@sttts — thanks for the Codex xhigh pass and the two inline threads. Fixes are in amended commit P1 — Agreed. Fix: P2 — DCGM blank-sentinel fallback Agreed. When Fix: reject any numeric power limit P2 — Helm unittest only in PR CI Fix: mirrored into Inline — K8s list client timeout ( Accepted. Removed Inline — Helm Makefile pattern ( Accepted. Added Architecture / smell notes (repo boundary, enum, AgentState, comment volume) Fair points — I don’t think this PR is the right place to relocate power-agent or collapse the DCGM state machine. The Validation
Let me know if you want any of the smell/architecture items split into follow-up issues before merge. |
|
@nv-anants @sttts — re-review request. Rebased the six commits onto current Local re-validation at the new head:
@sttts — the two July 6 threads (pod-LIST client timeout, and the power-agent Helm Makefile/CI wiring) are addressed and verified live at the new head; inline replies point to the exact lines. Would appreciate a resolve/sign-off when you have a moment. |
|
/ok to test 2567d2c |
|
Another pass with Codex xhigh: [P1] SIGTERM cleanup can be missed while blocked in pod LISTThe PR moves restore work out of the signal handler into Refs:
If SIGTERM lands while the Kubernetes client is stuck/throttled in the pod LIST, the handler only flips the event. Cleanup cannot run until the LIST returns. If kubelet reaches the 30s grace limit first, the process is SIGKILLed and managed caps are not restored. Suggested fix: keep heavy restore work out of the handler, but make pre-cleanup blocking calls bounded/cancellable. At minimum, add a client-side LIST timeout shorter than |
|
/ok to test bd3da17 |
|
@sttts thanks, agreed — fixed in The SIGTERM path now has three protections:
Regression coverage:
Validation at
Would appreciate your approval/sign-off if this addresses the P1. |
|
@nv-anants gentle approval nudge for #9790. The Power Agent shutdown fix is now on head The remaining full-PR CI reds are unrelated to this diff:
Root cause: Relevant #9790 checks are green:
@sttts also got the SIGTERM/pod-LIST fix explanation at |
nv-anants
left a comment
There was a problem hiding this comment.
workflow and dockerfile changes look
Adds the DCGM-backed actuator alongside the default NVML actuator behind an opt-in selector, plus managed-state tracking, orphan recovery, and the supporting Helm/values wiring and tests. Rebuilt cleanly on top of main after the parent PR #9682 landed via squash merge: replayed pr1b's net contribution over pr1a (the power-agent component in deploy/, the helm chart, and the .lycheeignore entries) onto current main. main's power-agent component is byte-identical to the old pr1a tip, so the result is conflict-free and the resulting tree matches the prior branch head for all touched paths. Signed-off-by: Kai Ma <kaim@nvidia.com>
…p writes Closes the DCGM reconnect/re-enumeration races from the #9790 review by requiring a stable GPU identity for every cap write and refusing the write whenever that identity cannot be established. apply_cap captures the entry-time identity FIRST, before any other reconnect-capable DCGM call (including constraints_w), and REQUIRES it: if get_uuid fails it ticks apply_failures_total and returns the clamped watts without writing. The cap value is derived from the workload on that index; without knowing which physical GPU the index hosts we cannot guarantee the cap lands on the GPU that produced it. Capturing before constraints_w matters because a reconnect inside the constraints read could re-enumerate the index -- taking the identity first (and re-verifying it in-transaction) turns that into a detected mismatch instead of silently adopting the new occupant as "expected". _write_set ALWAYS reads the GPU UUID via _read_uuid_raw immediately before the Set, inside the same _with_reconnect transaction (after any re-init), and returns it alongside the wattage. Bookkeeping records that transaction-local identity directly -- there is no post-Set get_uuid re-read that a hostengine restart could re-enumerate onto a different GPU and persist as the wrong UUID (the original F1 leak). The captured entry UUID is re-verified against this read; a proven mismatch fails closed. The write FAILS CLOSED on any identity it cannot read in-transaction too: a cap that cannot be attributed to a stable UUID cannot be persisted for orphan recovery, relocated on re-enumeration, or safely restored. This removes the earlier index-only fallback, which was unsafe -- at SIGTERM an index whose UUID was never recorded would be restored unguarded, un-capping the WRONG GPU if the index had re-enumerated while the real one leaked. restore_default and restore_default_by_uuid pass the resolved UUID as expected_uuid and treat _GpuIdentityMismatch as "not conclusively located" -- return False so the SIGTERM/release caller keeps the UUID (never prunes a still-live cap) and cold-start orphan recovery retries. A "default" written onto a re-enumerated GPU is not harmless: it clobbers a below-default cap another workflow owns, so restores fail closed too. The restore_default_by_uuid NO-write path (index already at/above default) now reverifies the index still hosts the UUID AFTER the reconnect-capable current_w / default_w reads before returning None: a re-enumeration between UUID resolution and those reads could otherwise let a replacement GPU (already at default) satisfy the branch while the GPU we own moved elsewhere with its cap still live, and returning None would prune ownership and leak the cap. On a mismatch or an unreadable recheck it returns False (retain ownership); only a reconfirmed identity at/above default prunes. The GPU identity now travels with the policy decision end-to-end, closing the wider window between PID attribution and the cap write. The reconcile loop captures the UUID BEFORE the PID snapshot that produces the cap and threads it into apply_cap via a new optional expected_uuid argument on the Actuator Protocol. apply_cap uses the caller-supplied identity verbatim (no entry re-capture, which would reopen the very window) and re-verifies it in-transaction before the Set; a re-enumeration anywhere across attribution -> resolve -> write is detected as a mismatch and the write skipped. If the identity is unreadable at capture time the reconcile loop skips the GPU this cycle and retries next reconcile. The NVML actuator accepts the argument for Protocol uniformity but does not need it -- it binds by index for the process lifetime and has no re-enumeration model. The release path is anchored to the same identity and fails closed on every reconnect-capable step. _reconcile_gpu threads the pre-snapshot expected_uuid into _release_managed_gpu, which now: * reverifies the current occupant matches that snapshot before acting; * decides on the CURRENT occupant's UUID, not the stale index->UUID map. When a re-enumeration moved a DIFFERENT physical GPU onto the index, if that current occupant is itself one we manage (UUID in the persisted set) it is released via the UUID-addressed restore_default_by_uuid (which resolves its live index and guards the write) -- this fixes the index- SWAP deadlock where two managed GPUs traded indices and the old index- based skip blocked BOTH releases until shutdown. If the current occupant is NOT one we manage, the stale projection is skipped without restoring or pruning (the GPU we capped is handled at its own current index); * re-reads and re-verifies the identity AFTER the reconnect-capable default_w / current_w reads and BEFORE restoring or pruning, so a re-enumeration during those reads cannot make current_w read a different GPU "already at default", skip the restore, and then prune our managed GPU whose cap is still live -- on mismatch or an unreadable identity it retains ownership and retries; * fails closed when the DCGM managed-identity lookup itself raises, rather than falling back to the current occupant (which on a re-enumerated index would bypass the stale-projection guard and release/prune on stale integer membership). NVML binds by index and has no managed_uuid_for_idx, so these dcgm-specific guards are no-ops there. reconcile_once re-snapshots self._actuator.device_count() every cycle instead of trusting the value cached at startup. A DCGM hostengine reconnect rebuilds the discovered-GPU set: a startup-frozen count would never reconcile GPUs added by a reconnect, and would raise per-index errors if the set shrank. The refresh is best-effort -- a transient read failure keeps the last-known count for the cycle rather than skipping enforcement. Graceful shutdown is split so the OS signal path stays async-signal-safe: _handle_sigterm only sets a shutdown event, and the reconcile loop's finally block runs _shutdown_cleanup, which performs the default-TGP restore, the UUID sweep, and ownership retirement. Docs, Helm comments, and the DaemonSet terminationGracePeriod note are corrected to say the reconcile loop (not the signal handler itself) restores caps on shutdown. Ownership retirement is durable across a persistence failure without repeating the hardware release. _release_managed_gpu delegates the release/attribution decision to the identity-bound restore_default_by_uuid, then _commit_release retires in-memory ownership and, if the durable persist fails, records the UUID in _pending_retirement; reconcile_once calls _flush_pending_retirements at the top of each cycle to retry only the persist step (never the un-cap) until it succeeds. The applied_limit_watts gauge no longer strands on a stale cap: it is re-synced to the live value on every restore and on the restore_default_by_uuid no-write path (GPU already at/above default), in both the NVML and DCGM actuators. _resolve_idx_for_uuid runs its UUID scan inside a single _with_reconnect transaction and restarts the scan on CONNECTION_NOT_VALID, so a hostengine reconnect that grows the topology mid-scan is retried instead of missing a newly enumerated GPU. Helm values/NOTES/templates are aligned with the actuator abstraction: NVML mode never loads or connects to DCGM though the image still bundles the DCGM bindings; DCGM mode requires DCGM 4.x; NOTES lists all six exposed metrics; and TGP terminology and source references are made consistent. Pseudo-version and review-provenance comments are removed from shipped code and tests in favour of invariant descriptions. Adds/updates regression tests for transaction-verified bookkeeping, fail-closed refusal on an unreadable entry identity, identity captured before a constraints_w re-enumeration, fail-closed refusal on an unreadable in-transaction recheck, proven mid-write mismatch on apply and both restore paths, fail-closed restore on an unreadable recheck, identity captured before the PID snapshot and threaded into apply_cap, fail-closed skip when that identity is unreadable, the actuator honoring a caller-supplied expected_uuid (match writes, mismatch refuses), the release path skipping a re-enumerated stale projection while still releasing the same GPU and bailing on a snapshot mismatch, retaining ownership on a re-enumeration during the power reads, failing closed when the managed-UUID lookup raises, releasing BOTH GPUs by UUID when two managed GPUs swap indices, re-snapshotting the device count each cycle (growth, shrink, and read- failure fallback), the UUID-sweep no-write path failing closed on a re-enumeration (identity mismatch or unreadable) between UUID resolution and the power reads, the indexed shutdown restore retiring its UUID before the sweep, deferred retirement retrying persistence without repeating the hardware release, restore paths re-syncing the applied-limit gauge, and _resolve_idx_for_uuid rescanning a topology that grew across a reconnect. 277 power-agent unit tests pass in the shipped image. Signed-off-by: Kai Ma <kaim@nvidia.com>
…tence durable
Follow-up hardening from four review passes on the DCGM actuator. Every
accept was grounded in the code path it touches; the through-line is that
cold-start orphan recovery and the persistence queues must never act on
ambiguous evidence.
Conclusive-identity orphan recovery:
- Adds scan_uuid_index_map() to the Actuator Protocol: a single
{uuid: index} snapshot plus a `conclusive` flag. NvmlActuator does a
clean pass over nvmlDeviceGetCount() (inconclusive on any read error);
DcgmActuator builds the map inside one _with_reconnect transaction so a
reconnect that grows the topology mid-scan is retried, and reports
INCONCLUSIVE for an empty discovered set (0 GPUs is far likelier to be a
dropped hostengine connection than a genuinely GPU-less node, and pruning
every persisted UUID there would fail open into a cap leak).
- _restore_orphaned_gpus_on_startup now resolves identities from that one
snapshot instead of a per-index get_uuid loop, restores only the
intersection of persisted UUIDs and currently-visible idle GPUs, and
prunes state-only "absent" UUIDs ONLY when the scan is conclusive. This
closes the false-gone prune after a DCGM re-enumeration/topology growth.
Read-failure and persistence durability:
- _read_managed_gpus_state returns (uuids, conclusive) so a transient read
error can no longer be misread as an empty managed set and rewrite the
file empty; an inconclusive load skips recovery entirely this boot.
- A startup persist failure now queues retired UUIDs into
_pending_retirement (retried by the reconcile loop) instead of dropping
the retirement.
Parity harness + docs:
- e2e_actuator_parity joins NVML vs DCGM results by GPU UUID
(nvidia-smi -i <UUID>) instead of positional zip, so a re-enumeration
between probes cannot pass a mismatched comparison.
- applied_limit_watts help text reworded to drop the stale/observed
contradiction; values.yaml and Dockerfile DCGM-version notes made
version-neutral (pin the operator's nvidia-dcgm tag; client >= hostengine).
Tests: adds test_parity_harness.py (GPU-less parity logic) and
test_pending_persistence.py (acquisition/retirement queue behavior),
extends the NVML/DCGM protocol and orphan-recovery suites for the new
snapshot and inconclusive-load paths. 306 power-agent unit tests pass.
Signed-off-by: Kai Ma <kaim@nvidia.com>
- Dockerfile: bump the vendored DCGM image to 4.5.3-1-ubuntu24.04 (ubuntu24 per reviewer). Verified the ubuntu24.04 libdcgm.so.4 loads cleanly on the python:3.12-slim-bookworm runtime (glibc 2.36) via ctypes.CDLL + import pydcgm, and the in-image unit suite (306 tests) passes on the new base. - Dockerfile: reference #11192 in the logger-shim comment so the vendored logger.py shim can be dropped once upstream DCGM packages logger.py alongside its pydcgm bindings. - .lycheeignore: drop the three stale deployment-guide ignore entries. The docs site restructured those paths (deploy-models/, advanced-platform/, start-here/); the README already links the canonical URLs (verified 200) and the old paths are no longer referenced in-tree, so the entries were dead weight. Signed-off-by: Kai Ma <kaim@nvidia.com>
… note Addresses PR #9790 review follow-ups. The vendored pydcgm bindings guard `import logger` behind `__DCGM_TESTING_FRAMEWORK_ACTIVE` (DcgmGroup.py / DcgmDiag.py import logger only when that env var == "1", else set `logger = None`). The Power Agent never sets it, so: - `import logger` never runs in production; there is no ModuleNotFoundError and DcgmGroup construction succeeds without the shim (verified: `import DcgmGroup` exits 0 with logger.py absent); - the shim is never imported (verified: with logger.py present and the env unset, `DcgmGroup.logger is None`) -- it was dead code. The only `logger.` calls are four `logger.debug(...)` in `DcgmGroup.__del__`, whose exceptions Python ignores. Remove `deploy/power-agent/logger.py` and its `COPY` line; the prior comment's "every DcgmGroup construction fails with ModuleNotFoundError" was inaccurate. Also reword the DCGM_IMAGE comment: the vendor image does not set the runtime OS, but the extracted libdcgm.so must stay ABI-compatible with bookworm's glibc; require the ctypes.CDLL + import pydcgm smoke (not just manifest inspect) for future tag overrides. Validation: `docker build --target test` -> 306 passed on 4.5.3-1-ubuntu24.04 with the shim removed. Signed-off-by: Kai Ma <kaim@nvidia.com>
- Re-probe device_count when DCGM topology is empty; restore outcome semantics and dedup-by-pod-UID fixes from prior review rounds. - Move pending_acquisition into managed_state.pending_acquisition so actuator writes via import power_agent and reconcile flushes in __main__ share one retry queue; regression tests for shared-object identity. - Reject DCGM numeric blank power-limit sentinels (>= 0x7FFFFFF0) without importing dcgmvalue; regression test with dcgmvalue unavailable. - Remove client-side K8S_LIST_TIMEOUT_S on pod LIST; keep resource_version="0" watch-cache reads and rely on apiserver backpressure. - Add deploy/helm/charts/power-agent/Makefile; run make lint test in PR and post-merge CI, matching the platform chart pattern. Validation: 314 power-agent tests passed; helm unittest 42 passed. Signed-off-by: Kai Ma <kaim@nvidia.com>
Bound pod LIST calls and enforce a 60s termination grace budget so SIGTERM cleanup has time to restore managed GPU caps. Signed-off-by: Kai Ma <kaim@nvidia.com>
|
/ok to test b416308 |
|
@sttts @nv-anants — heads-up on a rebase, no content change. I rebased this branch onto current
Your approvals carried over onto the new head, so nothing is required from you — this is just a courtesy notice. If you'd like to re-confirm given the rebase, that's welcome, but I don't want to reset anything unnecessarily. Thanks again for the reviews. |
Power Agent DCGM actuator (PR 1b of the per-node power-cap stack).
Predecessor: #9682 ΓÇö Power Agent NVML DaemonSet ΓÇö has landed on
main(squash82f5389f51). Base ismain. The branch has been rebased onto currentmain(headbd3da174a2); it is conflict-free and GitHub reportsMERGEABLE.Summary
Adds an opt-in DCGM actuator to the Power Agent introduced in #9682. Operators running the GPU Operator with
dcgm.enabled=truecan setagent.actuator=dcgmso caps are written through the clusternvidia-dcgmhostengine. Default NVML behavior is unchanged for operators that do not opt in; the implementation is refactored behind anActuatorprotocol and a shared managed-state module, with a transaction-verified GPU UUID anchoring every DCGM cap write against re-enumeration.Branch shape: 34 files changed vs
main, in 7 commits (headbd3da174a2). Nodocs/changes.Commits (post-rebase SHAs):
943fc660ΓÇöfeat(power-agent): add DCGM dual actuator (opt-in; NVML remains default)ΓÇö the actuator protocol, NVML/DCGM implementations, managed-state module, Helm wiring, and tests.afab6220ΓÇöfix(power-agent): fail closed on unverifiable GPU identity in DCGM cap writesΓÇö every DCGM cap write/restore is anchored to a transaction-verified GPU UUID and fails closed when identity cannot be established; the reconcile loop restores caps on shutdown; ownership retirement is durable across a persistence failure without repeating the hardware release.bae46171ΓÇöfix(power-agent): make orphan recovery identity-conclusive and persistence durableΓÇö cold-start orphan recovery resolves identities from one conclusivescan_uuid_index_map()snapshot, transient read failures no longer erase persisted state, and acquisition/retirement persistence is retried by the reconcile loop.ed2de2e6ΓÇöchore(power-agent): address PR #9790 review feedbackΓÇö bumps the vendored DCGM image to4.5.3-1-ubuntu24.04(reviewer asked for ubuntu24; validated the ubuntu24.04libdcgm.so.4loads on thepython:3.12-slim-bookwormruntime and the in-image suite passes on the new base) and removes the three now-stale.lycheeignoredeployment-guide entries (net-zero vsmain).b20a2c66ΓÇöchore(power-agent): remove inert DCGM logger shim; clarify vendor ABI noteΓÇö the vendored pydcgm bindings guardimport loggerbehind__DCGM_TESTING_FRAMEWORK_ACTIVE(never set by the agent), so thelogger.pyshim was never imported (dead code); removed it and itsCOPY, corrected the inaccurate Dockerfile comment, and tightened theDCGM_IMAGEABI guidance to require a load-smoke for future tag overrides.2567d2c6ΓÇöfix(power-agent): close DCGM zero-topology gaps and address sttts reviewΓÇö re-probesdevice_count()when the DCGM topology reads empty and restores the outcome-semantics / dedup-by-pod-UID fixes; movespending_acquisitionintomanaged_state.pending_acquisitionso the actuator'simport power_agentcopy and the__main__reconcile flush share one retry queue (with shared-object identity tests); rejects DCGM numeric blank power-limit sentinels (>= 0x7FFFFFF0) without importingdcgmvalue; removes the client-sideK8S_LIST_TIMEOUT_S/_request_timeouton pod LIST while keepingresource_version="0"watch-cache reads; and addsdeploy/helm/charts/power-agent/Makefile, wired into PR and post-merge CI viamake -C deploy/helm/charts/power-agent lint test.bd3da174a—fix(power-agent): bound pod listing during shutdown— bounds pod LIST calls with a 20s apiserver timeout and 25s best-effort client timeout, disables transport retries on the Power Agent's dedicated Kubernetes client, skips new reconcile work once SIGTERM is requested, and makes the Helm termination grace period configurable with a validated 60s minimum.The
--dcgm-enforceflag,dcgm_enforce_failures_totalmetric,agent.dcgm.enforcevalue, and thevalidate_enforcehelm suite that appeared in earlier revisions of this branch have been removed; the shipped surface is the three CLI flags and six metrics listed below.Changes:
Power Agent implementation (
deploy/power-agent/):actuator.py:Actuatorprotocol,NvmlActuator, andDcgmActuator. UUID-keyed identity mapping,_with_reconnectforDCGM_ST_CONNECTION_NOT_VALID,_GpuIdentityMismatchapply-time re-verification, andscan_uuid_index_map()returning a single{uuid: index}snapshot plus aconclusiveflag (DCGM reports an empty discovered topology as INCONCLUSIVE so a dropped hostengine connection cannot masquerade as a GPU-less node and prune every persisted UUID).power_agent.py: reconcile, SIGTERM, and orphan recovery dispatch through the active actuator; CLI exposes--actuator {nvml,dcgm},--dcgm-host,--dcgm-port. Orphan recovery restores only persisted UUIDs that are currently visible and idle, and prunes state-only "absent" UUIDs only when the identity scan is conclusive._read_managed_gpus_state()returns(uuids, conclusive)so a transient read error cannot rewrite the state file empty. Acquisition and retirement failures queue to_pending_acquisition/_pending_retirementand are retried (persist-only, never a repeated un-cap) at the top of each reconcile cycle. The SIGTERM path runs a UUID-complete sweep so a re-enumerated index cannot strand a cap.managed_state.py: single source of truth for the mutable managed-GPU sets (managed_gpu_indices,previously_managed,pending_acquisition) so the__main__and importedpower_agentmodule copies do not split state.Dockerfile: vendors pydcgm bindings andlibdcgm.sofrom the DCGM image for the opt-in path; the default NVML build is unaffected.DCGM_IMAGEdefaults to4.5.3-1-ubuntu24.04; version guidance is otherwise neutral (pin the operator'snvidia-dcgmtag; client >= hostengine), with a load-smoke required for tag overrides.tests/: power-agent unit suites ΓÇö actuator-protocol satisfaction, CLI/actuator selection, DCGM actuator behavior, orphan recovery, pending-persistence queues, reconcile wiring, reconcile fail-safe, SIGTERM dispatch, shared-state identity, managed-state parsing, multi-DGD topology, and GPU-less parity-harness logic.e2e_actuator_parity.pyis real-GPU parity scaffolding (joins NVML vs DCGM by UUID, not by index), excluded from CI by default.Helm chart (
deploy/helm/charts/power-agent/):Chart.yaml: chart/appVersion bump.Makefile:lint(helm lint + render prod/dev paths) andtest(helm-unittest) targets, invoked by PR and post-merge CI.values.yaml: addsagent.actuatorandagent.dcgm.{host,port}; the default keepsactuator=nvml. The DCGM-version note references the operator'snvidia-dcgmtag.templates/daemonset.yaml,templates/dev-pod.yaml,templates/role.yaml,templates/NOTES.txt: render actuator/DCGM flags and dev-mode mounts; NOTES lists the six exposed metrics and the effective RBAC scope.templates/_helpers.tpl: validates actuator and image pinning (:tagvs@sha256:digest) at template time.tests/validate_{actuator,image_tag,dev_image_tag}_test.yaml: helm-unittest coverage.CI (
.github/workflows/pr.yaml,.github/workflows/post-merge-ci.yml): the Power Agent job builds the runtime image, runs the in-imageteststage, and runsmake -C deploy/helm/charts/power-agent lint test.Exposed Prometheus metrics (six):
applied_limit_watts,multi_pod_gpu_total,safe_default_applied_total,apply_failures_total,cap_clamped_total,k8s_list_failures_total.Reviewer start points:
deploy/power-agent/actuator.pyΓÇö actuator protocol, DCGM UUID-identity handling,scan_uuid_index_map().deploy/power-agent/power_agent.pyΓÇö reconcile dispatch, orphan recovery, metric semantics, SIGTERM routing (incl. the UUID sweep), and pending-persistence retries.deploy/helm/charts/power-agent/ΓÇö values, helpers, templates, Makefile, and tests.Validation
Validated at
bd3da174a25f71b4ea0721cff4100825f9d05782:python3.10 -m pytest deploy/power-agent/tests/ -q: 319 passed on the host.python3.10 -m pytest deploy/power-agent/tests/test_reconcile_failsafe.py -q: 16 passed (focused coverage for LIST timeouts, transport retries, and shutdown fast-path).helm lint deploy/helm/charts/power-agentplus prod/devhelm templaterender checks: passed.helm unittest deploy/helm/charts/power-agent: 48 passed across 4 suites (actuator, image_tag, dev_image_tag, terminationGracePeriodSeconds).git diff --checkon the intended patch files: clean.pytest-marker-reportfails only on the Windows workstation due to the known POSIX-onlyfcntlimport in the repo's global test harness (unrelated to this diff); confirmed the focused suite andfcntlimport pass on Linux (WSL).2567d2c66c5c7d17425b09ccc838874690c2d25a(docker build --target test, python:3.12-slim base + baked deps): 314 passed. The latestbd3da174a2commit only adds bounded pod LIST/shutdown handling and Helm grace-period validation; full PR CI is being refreshed for this head.main(was 120 commits behind at rebase time). The only conflict was in.lycheeignore(main added a bitnami ignore entry; this branch adds then removes three deployment-guide entries across two commits) — resolved so the net tree keeps main's bitnami entry and drops the three deployment-guide entries.The heavy GitLab-mirror CI (Power Agent image build, tests-in-image, Helm, deploy tests) needs to run on
bd3da174a2; the local artifact/source checks above were reproduced locally.Merge Strategy
Seven commits rebased onto current
main; conflict-free. Squash- or rebase-merge are both fine.