Skip to content

feat(power-agent): add DCGM dual actuator (opt-in; NVML remains default) - #9790

Merged
kaim-eng merged 7 commits into
mainfrom
pr1b/power-agent-dcgm-actuator
Jul 8, 2026
Merged

feat(power-agent): add DCGM dual actuator (opt-in; NVML remains default)#9790
kaim-eng merged 7 commits into
mainfrom
pr1b/power-agent-dcgm-actuator

Conversation

@kaim-eng

@kaim-eng kaim-eng commented May 20, 2026

Copy link
Copy Markdown
Contributor

Power Agent DCGM actuator (PR 1b of the per-node power-cap stack).

Predecessor: #9682 ΓÇö Power Agent NVML DaemonSet ΓÇö has landed on main (squash 82f5389f51). Base is main. The branch has been rebased onto current main (head bd3da174a2); it is conflict-free and GitHub reports MERGEABLE.

Summary

Adds an opt-in DCGM actuator to the Power Agent introduced in #9682. Operators running the GPU Operator with dcgm.enabled=true can set agent.actuator=dcgm so caps are written through the cluster nvidia-dcgm hostengine. Default NVML behavior is unchanged for operators that do not opt in; the implementation is refactored behind an Actuator protocol 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 (head bd3da174a2). No docs/ changes.

Commits (post-rebase SHAs):

  1. 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.

  2. 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.

  3. bae46171 ΓÇö fix(power-agent): make orphan recovery identity-conclusive and persistence durable ΓÇö cold-start orphan recovery resolves identities from one conclusive scan_uuid_index_map() snapshot, transient read failures no longer erase persisted state, and acquisition/retirement persistence is retried by the reconcile loop.

  4. ed2de2e6 ΓÇö chore(power-agent): address PR #9790 review feedback ΓÇö bumps the vendored DCGM image to 4.5.3-1-ubuntu24.04 (reviewer asked for ubuntu24; validated the ubuntu24.04 libdcgm.so.4 loads on the python:3.12-slim-bookworm runtime and the in-image suite passes on the new base) and removes the three now-stale .lycheeignore deployment-guide entries (net-zero vs main).

  5. b20a2c66 ΓÇö chore(power-agent): remove inert DCGM logger shim; clarify vendor ABI note ΓÇö the vendored pydcgm bindings guard import logger behind __DCGM_TESTING_FRAMEWORK_ACTIVE (never set by the agent), so the logger.py shim was never imported (dead code); removed it and its COPY, corrected the inaccurate Dockerfile comment, and tightened the DCGM_IMAGE ABI guidance to require a load-smoke for future tag overrides.

  6. 2567d2c6 ΓÇö fix(power-agent): close DCGM zero-topology gaps and address sttts review ΓÇö re-probes device_count() when the DCGM topology reads empty and restores the outcome-semantics / dedup-by-pod-UID fixes; moves pending_acquisition into managed_state.pending_acquisition so the actuator's import power_agent copy and the __main__ reconcile flush share one retry queue (with shared-object identity tests); rejects DCGM numeric blank power-limit sentinels (>= 0x7FFFFFF0) without importing dcgmvalue; removes the client-side K8S_LIST_TIMEOUT_S/_request_timeout on pod LIST while keeping resource_version="0" watch-cache reads; and adds deploy/helm/charts/power-agent/Makefile, wired into PR and post-merge CI via make -C deploy/helm/charts/power-agent lint test.

  7. bd3da174afix(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-enforce flag, dcgm_enforce_failures_total metric, agent.dcgm.enforce value, and the validate_enforce helm 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:

  1. Power Agent implementation (deploy/power-agent/):

    • actuator.py: Actuator protocol, NvmlActuator, and DcgmActuator. UUID-keyed identity mapping, _with_reconnect for DCGM_ST_CONNECTION_NOT_VALID, _GpuIdentityMismatch apply-time re-verification, and scan_uuid_index_map() returning a single {uuid: index} snapshot plus a conclusive flag (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_retirement and 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 imported power_agent module copies do not split state.

    • Dockerfile: vendors pydcgm bindings and libdcgm.so from the DCGM image for the opt-in path; the default NVML build is unaffected. DCGM_IMAGE defaults to 4.5.3-1-ubuntu24.04; version guidance is otherwise neutral (pin the operator's nvidia-dcgm tag; 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.py is real-GPU parity scaffolding (joins NVML vs DCGM by UUID, not by index), excluded from CI by default.

  2. Helm chart (deploy/helm/charts/power-agent/):

    • Chart.yaml: chart/appVersion bump.

    • Makefile: lint (helm lint + render prod/dev paths) and test (helm-unittest) targets, invoked by PR and post-merge CI.

    • values.yaml: adds agent.actuator and agent.dcgm.{host,port}; the default keeps actuator=nvml. The DCGM-version note references the operator's nvidia-dcgm tag.

    • 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 (:tag vs @sha256: digest) at template time.

    • tests/validate_{actuator,image_tag,dev_image_tag}_test.yaml: helm-unittest coverage.

  3. CI (.github/workflows/pr.yaml, .github/workflows/post-merge-ci.yml): the Power Agent job builds the runtime image, runs the in-image test stage, and runs make -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-agent plus prod/dev helm template render checks: passed.
  • helm unittest deploy/helm/charts/power-agent: 48 passed across 4 suites (actuator, image_tag, dev_image_tag, terminationGracePeriodSeconds).
  • git diff --check on the intended patch files: clean.
  • Pre-commit on the touched files: isort, black, flake8, codespell, ruff, check-yaml, and the rest pass. pytest-marker-report fails only on the Windows workstation due to the known POSIX-only fcntl import in the repo's global test harness (unrelated to this diff); confirmed the focused suite and fcntl import pass on Linux (WSL).
  • Inside the shipped image at the previous head 2567d2c66c5c7d17425b09ccc838874690c2d25a (docker build --target test, python:3.12-slim base + baked deps): 314 passed. The latest bd3da174a2 commit only adds bounded pod LIST/shutdown handling and Helm grace-period validation; full PR CI is being refreshed for this head.
  • DCO: all seven commits are signed off.
  • Rebase: replayed the stack onto current 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.

@kaim-eng
kaim-eng requested review from a team as code owners May 20, 2026 16:42
@copy-pr-bot

copy-pr-bot Bot commented May 20, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added documentation Improvements or additions to documentation deployment::k8s Relates to dynamo deployment in kubernetes actions labels May 20, 2026
@kaim-eng kaim-eng changed the title Pr1b/power agent dcgm actuator feat(power-agent): add DCGM dual actuator (opt-in; NVML remains default) May 20, 2026
@kaim-eng
kaim-eng changed the base branch from main to pr1a/power-agent May 20, 2026 16:45
@github-actions github-actions Bot added the feat label May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This 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.

Changes

Power Agent Dual-Actuator System

Layer / File(s) Summary
Actuator Protocol and Implementations
components/power_agent/actuator.py, components/power_agent/logger.py, components/power_agent/tests/test_actuator_protocol.py, components/power_agent/tests/test_actuator_selection.py, components/power_agent/tests/test_dcgm_actuator.py, components/power_agent/tests/e2e_actuator_parity.py
Defines Actuator Protocol with lifecycle and cap control APIs. Implements NvmlActuator with lazy NVML imports and DcgmActuator with TCP hostengine connection, stale-handle recovery, and cross-library UUID mapping. Includes protocol conformance tests, factory selection tests, comprehensive DcgmActuator unit tests, and e2e parity harness comparing both actuators against nvidia-smi ground truth.
Power Agent Core and Reconciliation
components/power_agent/power_agent.py, components/power_agent/tests/test_reconcile_wiring.py, components/power_agent/tests/test_shutdown.py, components/power_agent/tests/test_orphan_recovery.py, components/power_agent/tests/test_managed_state_parser.py, components/power_agent/tests/test_multi_pod_policy.py
Implements PowerAgent with periodic GPU reconciliation (pod UID mapping, multi-pod cap policy, actuator-driven application), optional Prometheus metrics, SIGTERM-driven graceful shutdown routing through active actuator, and cold-start orphan recovery using UUID-gated managed GPU state. Tests validate actuator routing contract, shutdown behavior with failure isolation, orphan recovery logic, managed state persistence robustness, and policy fallback metrics semantics.
Container Image and Component Documentation
components/power_agent/Dockerfile, components/power_agent/README.md
Multi-stage Dockerfile supporting both NVML and DCGM with vendored DCGM Python bindings and libraries. Component README documents dual-actuator modes, Helm chart compatibility requirements, and troubleshooting including Prometheus metrics semantics.
Helm Chart Values and Configuration
deploy/helm/charts/power-agent/Chart.yaml, deploy/helm/charts/power-agent/values.yaml, deploy/helm/charts/power-agent/README.md
Chart metadata (v1.2.0, appVersion v1.1.0) and comprehensive values.yaml with image pinning (tag/digest), actuator selection, DCGM connection options, and dev-mode ConfigMap requirements. Chart README provides production and dev install guidance, image pinning rules, troubleshooting, and cross-writer flapping warnings.
Helm Template Validation and Deployment
deploy/helm/charts/power-agent/templates/_helpers.tpl, deploy/helm/charts/power-agent/templates/daemonset.yaml, deploy/helm/charts/power-agent/templates/dev-pod.yaml, deploy/helm/charts/power-agent/templates/NOTES.txt
Strict image validation helpers (tag/digest mutual exclusivity, :latest rejection, whitespace rejection, SHA-256 format enforcement) and actuator/enforce validators. imageRef helper renders canonical OCI references. DaemonSet and dev-Pod templates conditionally inject actuator and DCGM flags. Post-install notes guide mode-specific verification and iteration.
Helm Unittest Test Suites
deploy/helm/charts/power-agent/tests/validate_image_tag_test.yaml, deploy/helm/charts/power-agent/tests/validate_actuator_test.yaml, deploy/helm/charts/power-agent/tests/validate_enforce_test.yaml
46 unit test cases validating template-time behavior: image tag/digest mutual exclusivity, :latest rejection, whitespace/format validation; actuator selection (nvml default, dcgm opt-in, invalid rejection); dcgm-enforce boolean allowlist (yes/no/on/off/true/false with case normalization, rejection of invalid values, cross-validator behavior for unused modes).
Design Documentation
docs/design-docs/power-agent-helm-chart-plan.md
Comprehensive plan document updated from draft to implementation record, including v1.1.0 dual-actuator expansion and v1.2.0 image-pinning hardening. Detailed sections on chart schema, validation strategy, template-time validators, installation flows, commit gates, risk analysis, and pre-flight/post-push checklists.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed and aligned with the PR, but it omits the template's required Related Issues section. Add the Related Issues section and either link the issue(s) with Closes/Relates to #XXXX or check "Confirmed — no related issue".
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding an opt-in DCGM dual actuator while keeping NVML as the default.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

kaim-eng added a commit that referenced this pull request May 20, 2026
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>
kaim-eng added a commit that referenced this pull request May 20, 2026
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>
kaim-eng added a commit that referenced this pull request May 20, 2026
…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>
kaim-eng added a commit that referenced this pull request May 22, 2026
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>
kaim-eng added a commit that referenced this pull request May 22, 2026
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>
@kaim-eng
kaim-eng force-pushed the pr1a/power-agent branch from 0eda1d7 to 1e1ef6f Compare May 25, 2026 13:43
kaim-eng added a commit that referenced this pull request May 25, 2026
… 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>
kaim-eng added a commit that referenced this pull request May 25, 2026
… 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>
@kaim-eng
kaim-eng force-pushed the pr1a/power-agent branch from 1e1ef6f to 29f0831 Compare May 25, 2026 13:46
kaim-eng added a commit that referenced this pull request May 25, 2026
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>
kaim-eng added a commit that referenced this pull request May 25, 2026
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>
@kaim-eng
kaim-eng force-pushed the pr1b/power-agent-dcgm-actuator branch from 99138c4 to abcc922 Compare May 25, 2026 14:27
kaim-eng added a commit that referenced this pull request May 25, 2026
…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>
kaim-eng added a commit that referenced this pull request May 25, 2026
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>
kaim-eng added a commit that referenced this pull request May 25, 2026
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>
@kaim-eng

Copy link
Copy Markdown
Contributor Author

Rebased onto the refreshed pr1a/power-agent (tip 29f0831) and main (~316 commits forward from the prior base). Force-pushed pr1b/power-agent-dcgm-actuator to abcc922.

What moved:

CI rerunning now.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

@kaim-eng Thanks for the detailed rebase summary! The backport strategy—keeping each hardening fix with the code it touches on pr1a while retaining the actuator-aware refinements and the 29 new test cases here on pr1b—looks like a clean separation. I'll kick off a fresh review of the force-pushed tip now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add 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 win

Add 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 win

Add 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 win

Add 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 win

Strengthen 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 exact contains/notContains assertions 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=off

Also 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 win

Move function-scope imports to module scope.

from tests.test_multi_pod_policy import _FakeMetrics is 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 win

Hoist logging imports to module scope.

import logging is 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 win

Avoid silently swallowing NVML shutdown errors.

This except Exception: pass hides 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 Exception then 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

📥 Commits

Reviewing files that changed from the base of the PR and between 29f0831 and abcc922.

📒 Files selected for processing (26)
  • components/power_agent/Dockerfile
  • components/power_agent/README.md
  • components/power_agent/actuator.py
  • components/power_agent/logger.py
  • components/power_agent/power_agent.py
  • components/power_agent/tests/e2e_actuator_parity.py
  • components/power_agent/tests/test_actuator_protocol.py
  • components/power_agent/tests/test_actuator_selection.py
  • components/power_agent/tests/test_dcgm_actuator.py
  • components/power_agent/tests/test_managed_state_parser.py
  • components/power_agent/tests/test_multi_pod_policy.py
  • components/power_agent/tests/test_orphan_recovery.py
  • components/power_agent/tests/test_reconcile_wiring.py
  • components/power_agent/tests/test_shutdown.py
  • deploy/helm/charts/power-agent/Chart.yaml
  • deploy/helm/charts/power-agent/README.md
  • deploy/helm/charts/power-agent/templates/NOTES.txt
  • deploy/helm/charts/power-agent/templates/_helpers.tpl
  • deploy/helm/charts/power-agent/templates/daemonset.yaml
  • deploy/helm/charts/power-agent/templates/dev-pod.yaml
  • deploy/helm/charts/power-agent/tests/validate_actuator_test.yaml
  • deploy/helm/charts/power-agent/tests/validate_enforce_test.yaml
  • deploy/helm/charts/power-agent/tests/validate_image_tag_test.yaml
  • deploy/helm/charts/power-agent/values.yaml
  • docs/design-docs/power-agent-dual-actuator.md
  • docs/design-docs/power-agent-helm-chart-plan.md

Comment thread components/power_agent/actuator.py Outdated
Comment thread components/power_agent/actuator.py Outdated
Comment thread deploy/power-agent/actuator.py
Comment thread deploy/power-agent/power_agent.py
Comment thread deploy/helm/charts/power-agent/README.md Outdated
Comment thread deploy/helm/charts/power-agent/templates/_helpers.tpl Outdated
Comment thread docs/design-docs/power-agent-helm-chart-plan.md Outdated
Comment thread docs/design-docs/power-agent-helm-chart-plan.md Outdated
Comment thread docs/design-docs/power-agent-helm-chart-plan.md Outdated
@kaim-eng

kaim-eng commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 7226298

@kaim-eng

kaim-eng commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 3b84f22

@kaim-eng

kaim-eng commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 85df904

@kaim-eng

kaim-eng commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@nv-anants @sttts (cc @dynamo-docs) — re-review request.

Since the last review pass I pushed one commit, 85df9043. Unlike the earlier refreshes this is not a rebase — it adds small, focused changes from a final best-effort review:

  • fix (correctness): DcgmActuator.device_count() now re-probes hostengine discovery when the cached GPU topology is empty, so a transiently-empty startup (agent connects before nvidia-dcgm finishes enumerating) self-heals on the next reconcile instead of silently enforcing no caps until a restart. Regression tests added.
  • fix (observability): a DCGMError from the pre-write constraints read in apply_cap() is now absorbed into apply_failures_total (it previously escaped the metric); a malformed power-limit value is deliberately left to surface.
  • ci: added a helm unittest step to the power-agent job — the 42 chart tests existed but were never executed. Pinned to plugin v1.0.3 because v1.1.0's platformHooks manifest breaks the pinned Helm 3.17.3 (helm-unittest#856); verified locally against that exact Helm version.
  • docs/logs: corrected the values.yaml DCGM version (4.5.3), the actuator lazy-import list, the previously_managed comment, and the restore-outcome docstring/log wording.

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 85df9043 (/ok to test posted). Local gates: power-agent suite 311 passed, helm unittest 42 passed. Could you take another look when you have a moment? Thanks!

@kaim-eng

kaim-eng commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@sttts

sttts commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex xhigh review:

Findings

[P1] _pending_acquisition still has the same __main__ / import power_agent split bug

The PR correctly moves the main ownership sets into managed_state.py, but the acquisition retry queue remains module-local in power_agent.py.

Refs:

  • _pending_acquisition is local: deploy/power-agent/power_agent.py:267
  • failed durable ADD queues there: deploy/power-agent/power_agent.py:294
  • reconcile flushes its own module copy: deploy/power-agent/power_agent.py:1371
  • DCGM records via import power_agent: deploy/power-agent/actuator.py:1568, deploy/power-agent/actuator.py:1576
  • entrypoint comment says all shared mutable state lives in managed_state, but this queue does not: deploy/power-agent/power_agent.py:1703

Production launches power_agent.py as __main__, while the actuators import canonical power_agent. If a cap write succeeds but persisting managed_gpus.json fails, the UUID can be queued in the canonical module’s _pending_acquisition, while the running daemon flushes the __main__ queue. That retry never happens.

This affects DCGM directly and NVML too, because NvmlActuator.apply_cap also imports canonical power_agent before delegating: deploy/power-agent/actuator.py:425, deploy/power-agent/actuator.py:431.

Fix: move _pending_acquisition into managed_state.py, or make the entrypoint/import module canonical.

[P2] DCGM blank-sentinel fallback can silently accept blank power values

_coerce_power_limit_watts builds the blank sentinel set from dcgmvalue, but if dcgmvalue is missing or one constant is renamed, it catches ImportError / AttributeError and continues with an empty blank_values set: deploy/power-agent/actuator.py:1181, deploy/power-agent/actuator.py:1192.

The comment says finite/positive checks still reject dangerous values, but DCGM blank values are finite positive numbers. The file itself calls out DCGM_FP64_BLANK = 140737488355328.0: deploy/power-agent/actuator.py:1062. So in that fallback path, a blank current/default/min/max value can be treated as a real watt value.

Fix: include known numeric blank constants as fallback values, or fail closed if the blank constants cannot be loaded.

[P2] Helm unit tests run on PR but not post-merge

The PR adds helm unittest to .github/workflows/pr.yaml: .github/workflows/pr.yaml:274.

The post-merge Power Agent job still only does Helm lint/template: .github/workflows/post-merge-ci.yml:166.

That leaves the new chart test suite as PR-only coverage. I would mirror the same helm plugin install + helm unittest deploy/helm/charts/power-agent step in post-merge CI.

Old threads

The old SIGTERM / re-enumeration GPU-A leak looks fixed now:

  • append-only _capped_uuids: deploy/power-agent/actuator.py:622
  • UUID sweep after index restore: deploy/power-agent/power_agent.py:895
  • UUID-addressed restore: deploy/power-agent/actuator.py:1692

The other old findings I rechecked also look addressed:

  • device count refresh
  • no post-Set UUID reread
  • narrowed DCGM write exception handling
  • reconnect shutdown logging
  • logger shim removal

Architecture concern

Separate from code correctness: the repo-boundary concern still stands.

The hard Dynamo interface appears to be only dynamo.nvidia.com/gpu-power-limit: deploy/power-agent/power_agent.py:78.

The agent is already a standalone DaemonSet/Helm chart, and repo-wide search did not show Dynamo code outside the agent/chart producing or consuming that annotation. So the “why is this in Dynamo instead of a standalone GPU power-agent consumed as a subchart/dependency?” question remains valid.

@sttts

sttts commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

And a Codex smell check:

Code smell ranking

  • **** Split module state: __main__ vs import power_agent

    • Where: managed_state.py:4, power_agent.py:267, power_agent.py:1703, actuator.py:1568
    • Why it smells: the code already had to create managed_state.py because Python creates two module objects. But not all shared mutable state was moved there.
    • Fix difficulty: medium
    • Fix: move _pending_acquisition into managed_state.py, or better, stop importing power_agent from actuator.py and pass an explicit state object into the actuator.
  • **** Global mutable state + persistence queues

    • Where: power_agent.py:140, power_agent.py:267, power_agent.py:459, power_agent.py:469
    • Why it smells: ownership state, durable state, and retry queues are spread across module globals. Correctness depends on mutating the right global in the right module copy.
    • Fix difficulty: medium/hard
    • Fix: introduce an AgentState object that owns managed_gpu_indices, previously_managed, pending acquisitions, pending retirements, and persistence.
  • *** Tri-state Optional[bool] restore contract

    • Where: actuator.py:177, actuator.py:192, power_agent.py:840, power_agent.py:910, power_agent.py:1041
    • Why it smells: True, None, and False encode three semantic outcomes, and call sites must remember which values mean prune vs retain.
    • Fix difficulty: medium
    • Fix: replace with an enum like RESTORED, NOT_NEEDED, INCONCLUSIVE.
  • *** DCGM identity/re-enumeration state machine is spread across too many paths

    • Where: power_agent.py:581, power_agent.py:953, actuator.py:1221, actuator.py:1692, actuator.py:1927
    • Why it smells: the same invariant appears in reconcile, release, shutdown, orphan recovery, and DCGM internals: resolve UUID, verify identity, write or skip, retain or prune.
    • Fix difficulty: hard
    • Fix: centralize the UUID-bound restore/write logic into named helpers or a transaction-style object.
  • *** Huge comments are carrying correctness

    • Where: managed_state.py:4, power_agent.py:581, power_agent.py:953, actuator.py:1221, actuator.py:1692
    • Why it smells: many comments are explaining how not to break subtle invariants. That usually means the code shape does not encode the invariant strongly enough.
    • Fix difficulty: medium/hard
    • Fix: move invariants into types, enums, smaller state-transition functions, and explicit state objects.
  • *** PR mixes refactor, feature, and hardening

    • Where: whole PR
    • Why it smells: reviewers must validate the Actuator refactor, the new DCGM backend, and generic ownership/recovery bugfixes at the same time.
    • Fix difficulty: hard now, easy if split earlier
    • Fix: ideal split would be: Actuator refactor first, NVML-only ownership/recovery hardening second, DCGM backend third.
  • *** Agent/repo boundary is unclear

    • Where: power_agent.py:78, deploy/helm/charts/power-agent
    • Why it smells: the hard Dynamo interface appears to be only dynamo.nvidia.com/gpu-power-limit, while the agent is otherwise standalone.
    • Fix difficulty: hard/org-level
    • Fix: decide whether this is Dynamo-owned or a general NVIDIA GPU power agent. If general, move it to its own repo with a neutral annotation/API and consume it from Dynamo as a Helm subchart or external dependency.
  • ** Chart test coverage not mirrored post-merge

    • Where: .github/workflows/pr.yaml:274, .github/workflows/post-merge-ci.yml:166
    • Why it smells: new chart tests run on PR but not after merge.
    • Fix difficulty: easy
    • Fix: copy the helm unittest step into post-merge CI.
  • ** DCGM blank sentinel handling has fragile fallback

    • Where: actuator.py:1181, actuator.py:1192
    • Why it smells: if dcgmvalue constants are unavailable, finite positive DCGM blank values may pass as real watt values.
    • Fix difficulty: easy/medium
    • Fix: add known fallback blank numeric constants or fail closed when blank constants cannot be loaded.

Comment thread deploy/power-agent/power_agent.py Outdated
Comment thread .github/workflows/pr.yaml Outdated
@kaim-eng

kaim-eng commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@sttts — thanks for the Codex xhigh pass and the two inline threads. Fixes are in amended commit 551d4f7789 (local; force-push pending).

P1 — _pending_acquisition __main__ / import power_agent split

Agreed. _managed_gpu_indices and _previously_managed were already on managed_state, but _pending_acquisition was still module-local, so DcgmActuator writes via import power_agent queued on the canonical module while reconcile_once in __main__ flushed an empty copy.

Fix: managed_state.pending_acquisition is now the single queue; power_agent.py aliases _pending_acquisition to it. Regression tests in test_managed_state_sharing.py assert shared-object identity across both import paths and that a flush in __main__ drains writes from the canonical module. (_pending_retirement was already safe — writers and flusher both live in __main__.)

P2 — DCGM blank-sentinel fallback

Agreed. When dcgmvalue import fails we used to leave blank_values empty, and DCGM_FP64_BLANK is finite/positive so it could slip through.

Fix: reject any numeric power limit >= 0x7FFFFFF0 (_DCGM_NUMERIC_BLANK_MIN) without importing dcgmvalue. Regression test explicitly sets dcgmvalue=None under patch.dict(..., clear=False) so the already-loaded module path is exercised.

P2 — Helm unittest only in PR CI

Fix: mirrored into post-merge-ci.yml, and both workflows now run make -C deploy/helm/charts/power-agent lint test (see inline reply on pr.yaml).

Inline — K8s list client timeout (power_agent.py)

Accepted. Removed K8S_LIST_TIMEOUT_S / _request_timeout on pod LIST calls. We still pass resource_version="0" for watch-cache reads; under apiserver pressure we now rely on server-side backpressure instead of a fixed client timeout that could amplify retry storms. Tests updated accordingly.

Inline — Helm Makefile pattern (pr.yaml)

Accepted. Added deploy/helm/charts/power-agent/Makefile with lint + test targets; PR and post-merge CI call make -C deploy/helm/charts/power-agent lint test, same shape as the platform chart. We pin HELM_UNITTEST_VERSION ?= v1.0.3 here (platform still uses 0.7.2) because this chart’s CI image is on Helm 3.17.3.

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 ActuatorKind enum cleanup is tracked in #11193. Happy to discuss repo-boundary separately; for this stack I’m trying to keep the diff reviewable against #9682’s landing shape.

Validation

  • python3.10 -m pytest deploy/power-agent/tests -q → 314 passed
  • make -C deploy/helm/charts/power-agent lint test (WSL) → 42 helm unittests passed

Let me know if you want any of the smell/architecture items split into follow-up issues before merge.

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@nv-anants @sttts — re-review request.

Rebased the six commits onto current main (branch was 120 behind); new head 2567d2c6. The only conflict was .lycheeignore — resolved to keep main's new bitnami ignore entry and drop the three deployment-guide entries this branch already removes, so deploy/power-agent/** and deploy/helm/charts/power-agent/** are byte-identical to the pre-rebase tip.

Local re-validation at the new head:

  • python3.10 -m pytest deploy/power-agent/tests: 314 passed
  • in-image docker build --target test: 314 passed
  • helm unittest deploy/helm/charts/power-agent: 42 passed (3 suites)
  • pre-commit clean (the pytest-marker-report red is the known Windows-only fcntl artifact; exit 0 on Linux)
  • DCO: 6/6 signed

@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.

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 2567d2c

@sttts

sttts commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Another pass with Codex xhigh:

[P1] SIGTERM cleanup can be missed while blocked in pod LIST

The PR moves restore work out of the signal handler into run()'s finally, but reconcile_once() can still block before it reaches any shutdown check.

Refs:

  • deploy/power-agent/power_agent.py:799: _handle_sigterm() only sets _shutdown
  • deploy/power-agent/power_agent.py:1262: _list_pods_on_node()
  • deploy/power-agent/power_agent.py:1303: list_namespaced_pod(...) has no _request_timeout
  • deploy/power-agent/power_agent.py:1309: list_pod_for_all_namespaces(...) has no _request_timeout
  • deploy/power-agent/power_agent.py:1369: LIST happens before the GPU loop
  • deploy/power-agent/power_agent.py:1405: shutdown is checked only after LIST returns
  • deploy/power-agent/power_agent.py:1591: _shutdown_cleanup() only runs from finally
  • deploy/helm/charts/power-agent/templates/daemonset.yaml:42: pod only has 30s grace

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 terminationGracePeriodSeconds and treat timeout as the existing None failure path. Also add a shutdown guard before starting the LIST.

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test bd3da17

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@sttts thanks, agreed — fixed in bd3da174a.

The SIGTERM path now has three protections:

  1. reconcile_once() checks _shutdown before persistence flushes, pod LIST, or GPU work, so if SIGTERM has already landed we immediately return to run()'s finally cleanup.
  2. Pod LIST calls are bounded on both sides: timeout_seconds=20 for the apiserver and _request_timeout=25 as a best-effort client bound. Transport retries are disabled on this agent's dedicated Kubernetes client, so the timeout is not silently multiplied into retry load.
  3. Helm now defaults terminationGracePeriodSeconds to 60 and rejects values below 60, giving cleanup headroom after a worst-case in-flight LIST.

Regression coverage:

  • test_reconcile_failsafe.py asserts both LIST variants carry the dual bounds, the client timeout is >= the server timeout, transport retries are disabled, and the early shutdown guard skips persistence flushes and pod LIST.
  • Helm unittest now covers default/override/rejection for terminationGracePeriodSeconds in both DaemonSet and dev-pod modes.

Validation at bd3da174a:

  • python3.10 -m pytest deploy/power-agent/tests/ -q -> 319 passed
  • helm lint + prod/dev render + helm unittest -> 48 passed
  • git diff --check clean

Would appreciate your approval/sign-off if this addresses the P1.

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@nv-anants gentle approval nudge for #9790. The Power Agent shutdown fix is now on head bd3da174a2, the PR body is refreshed, and all Power Agent / Helm / Operator / Snapshot gates are green on this SHA.

The remaining full-PR CI reds are unrelated to this diff:

  • trtllm-runtime / Test cuda13.1, amd64
  • trtllm-runtime / Test cuda13.1, arm64
  • backend-status-check (aggregate failure because trtllm-test failed)

Root cause: components/src/dynamo/trtllm/tests/test_trtllm_multimodal_encoder.py builds a synthetic transformers ModuleType fixture and only defines AutoConfig on it, then imports dynamo.trtllm.engine, whose line 13 imports PretrainedConfig. That fails against the incomplete stub (ImportError: cannot import name 'PretrainedConfig' from 'transformers' (unknown location)). This fixture/test path comes from upstream TRTLLM changes already in the merge base; PR #9790 touches no components/, TRTLLM, transformers, or container files.

Relevant #9790 checks are green:

  • Power Agent -> success
  • Helm Chart Tests -> success
  • Operator / deploy-operator -> success
  • Snapshot Agent -> success
  • pre-commit / DCO / docs / Fern / PR-XPU guard -> success

@sttts also got the SIGTERM/pod-LIST fix explanation at bd3da174a2; waiting on reviewer sign-off now. Would appreciate your approval if the Power Agent changes look good from your side.

@nv-anants nv-anants left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workflow and dockerfile changes look

kaim-eng added 7 commits July 8, 2026 13:54
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>
@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test b416308

@kaim-eng

kaim-eng commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@sttts @nv-anants — heads-up on a rebase, no content change.

I rebased this branch onto current main (new head b416308d6f). The rebase was purely to pick up the multimodal-encoder test-fixture fix from #11383 (2c7ce4a8a3), which was the only thing turning full CI red — that failure was a pre-existing defect on main, not from this PR (this PR touches no components/, TRT-LLM, Transformers, or container files).

  • The 7 Power Agent / Helm commits are unchanged in content; only their parent moved.
  • Re-ran focused gates on the new head: 319 pytest + 48 Helm unittest all pass; every commit is DCO-signed.
  • Full CI re-triggered on b416308d6f.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

actions deployment::k8s Relates to dynamo deployment in kubernetes documentation Improvements or additions to documentation feat size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants