Skip to content

feat(power): validate single-node GPU energy metrics / 校验单节点 GPU 能耗指标 - #2322

Closed
edwingao28 wants to merge 3 commits into
SemiAnalysisAI:mainfrom
edwingao28:agent/validated-single-node-power
Closed

feat(power): validate single-node GPU energy metrics / 校验单节点 GPU 能耗指标#2322
edwingao28 wants to merge 3 commits into
SemiAnalysisAI:mainfrom
edwingao28:agent/validated-single-node-power

Conversation

@edwingao28

Copy link
Copy Markdown
Collaborator

Summary

  • Validate single-node, non-disaggregated fixed-sequence GPU-board power over the formal benchmark window.
  • Integrate each GPU independently with trapezoidal integration and boundary interpolation, then enforce expected GPU count, complete window coverage, bounded sample gaps, and valid telemetry values.
  • Preserve the existing per-GPU avg_power_w meaning and add whole-deployment avg_total_gpu_power_w, total_gpu_energy_j, J/successful query, and J/input, output, and total token.
  • Emit power_valid=0 plus an auditable validation sidecar when telemetry is invalid; REQUIRE_POWER=1 makes study/canary jobs fail after artifacts are written.
  • Upload the raw benchmark result, raw gpu_metrics.csv, aggregate JSON, and validation JSON as an independent power audit bundle.
  • Extend CPU CI coverage to the power aggregator, process-result integration, workflow contract, and NVIDIA monitor shutdown path.

This builds on the measured-power path introduced in #1558. It keeps the single-node aggregation scope separate from the multi-node and role-level work discussed in the closed #1635.

Scope

This PR covers single-node, non-disaggregated fixed-sequence serving, with H100/H200 8k/1k as the initial validation target.

Not included: GB200/GB300 multi-node or disaggregated telemetry, prefill/decode role attribution, srt-slurm changes, MI355X hardware validation, 4P1D/8P1D rack-scale experiments, repeated publication runs, plots, or article work.

Validation

  • python -m pytest utils/test_aggregate_power.py utils/test_process_result.py -q — 80 passed
  • Ruff check — passed
  • mypy — passed
  • Bash syntax, YAML parsing, and full diff checks — passed
  • H100/H200 strict 8k/1k canary — pending after draft PR creation

中文说明

  • 在正式基准测试窗口内,校验单节点、非分离式固定序列推理的 GPU 板级功耗。
  • 对每张 GPU 分别采用梯形积分和边界插值,并校验预期 GPU 数量、完整时间窗口覆盖、采样间隔上限和遥测数值有效性。
  • 保留现有按单卡计算的 avg_power_w 语义,并新增整套部署的 avg_total_gpu_power_wtotal_gpu_energy_j、每个成功查询的能耗,以及每个输入、输出和总 token 的能耗。
  • 遥测无效时输出 power_valid=0 和可审计的校验 sidecar;设置 REQUIRE_POWER=1 后,研究或 canary 任务会在写出产物后失败。
  • 通过独立的功耗审计产物上传原始基准测试结果、原始 gpu_metrics.csv、聚合 JSON 和校验 JSON。
  • 扩展 CPU CI,覆盖功耗聚合器、结果处理集成、workflow contract 和 NVIDIA 监控进程停止路径。

本 PR 基于 #1558 引入的实测功耗路径,并将单节点聚合范围与已关闭的 #1635 中讨论的多节点和角色级功能保持分离。

范围

本 PR 仅覆盖单节点、非分离式固定序列推理,初始验证目标为 H100/H200 8k/1k。

不包括:GB200/GB300 多节点或分离式遥测、预填充/解码角色归因、srt-slurm 修改、MI355X 硬件验证、4P1D/8P1D 机架级实验、用于发布的重复实验、绘图或文章工作。

验证

  • python -m pytest utils/test_aggregate_power.py utils/test_process_result.py -q — 80 项通过
  • Ruff check — 通过
  • mypy — 通过
  • Bash 语法、YAML 解析和完整 diff 检查 — 通过
  • H100/H200 严格模式 8k/1k canary — draft PR 创建后待运行

Integrate per-device telemetry over the formal benchmark window, enforce expected GPU topology, emit auditable validity artifacts, and add best-effort and strict power modes with CPU coverage.

中文:校验单节点 GPU 能耗指标。基于正式基准测试窗口对逐卡遥测数据进行积分,校验预期 GPU 拓扑,输出可审计的有效性产物,并为默认尽力模式和严格功耗模式补充 CPU 测试覆盖。
Treat unparseable timestamps and in-window power values as validation failures so best-effort runs emit power_valid=0 and strict runs fail without publishing Joule metrics. Add regression coverage for both cases.

中文:拒绝格式异常的 GPU 遥测样本。将无法解析的时间戳和窗口内功耗值视为校验失败,使尽力模式输出 power_valid=0,严格模式失败,并避免发布 Joule 指标;同时补充两个回归测试。
@edwingao28
edwingao28 marked this pull request as ready for review July 23, 2026 23:35
@edwingao28
edwingao28 requested a review from a team July 23, 2026 23:35

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@edwingao28

Copy link
Copy Markdown
Collaborator Author

@claude review

@Klaud-Cold

Klaud-Cold commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Claude finished @edwingao28's task in 5m 53s —— View job


Code Review

  • Gather context (read AGENTS.md, PR diff)
  • Review utils/aggregate_power.py (core power validation logic)
  • Review utils/process_result.py integration
  • Review workflow + shell changes
  • Review test coverage
  • Run tests / lint locally (pytest utils/test_aggregate_power.py utils/test_process_result.py — 80 passed; mypy on aggregate_power.py — clean)
  • Post review feedback

Summary

This is a well-built PR. The per-device trapezoidal integration with boundary interpolation is mathematically correct (I verified the test arithmetic), the validation reason codes are auditable, writes are atomic, the strict/best-effort split via REQUIRE_POWER is cleanly layered (artifacts always written before failing), and the test coverage is genuinely thorough. The findings below are ranked; #1 is the only one I'd consider addressing before the canary run, because it can silently corrupt the metric this PR exists to validate.

Findings

1. HIGH — stop_gpu_monitor final-sample append can concatenate onto a truncated line and produce a plausible bogus power sample (benchmarks/benchmark_lib.sh:163-172)

kill can interrupt the background nvidia-smi -l mid-write, leaving the CSV without a trailing newline. The post-kill nvidia-smi ... >> "$GPU_METRICS_CSV" then appends onto that partial line. I reproduced the resulting parse with csv.DictReader:

row: {'timestamp': '2026/07/23 10:00:01.000', 'power.draw [W]': '52026/07/23 10:00:02.000', ...}

Because _parse_power extracts the first number from the cell (utils/aggregate_power.py:129), "52026/07/23 …" parses as 52026.0 W — finite, non-negative, so it passes validation. The truncated row's timestamp is the last monitor tick, i.e. right at/inside the window end, so this corrupts the integrated energy near the boundary rather than tripping power_valid=0. It also swallows the real appended sample, which can additionally cause a spurious benchmark_window_not_bracketed.

Fix: before appending, repair a partial trailing line — e.g. if tail -c 1 "$GPU_METRICS_CSV" is not a newline, drop the last line (sed -i '$d') so the appended real sample restores end-bracketing on a clean row. Fix this →

2. MEDIUM — _parse_power has no plausibility bounds

Related to #1 but independently useful: a per-GPU board power of 52 kW (or 0 W stuck-at-zero from a wedged sensor) passes validation. Given this PR's whole purpose is validated power, consider a sanity envelope (e.g. reject/flag per-GPU samples above a few kW; optionally flag all-zero streams). A fullmatch-style pattern for the power cell (<number> [W] / bare number) instead of _NUMBER_RE.search would also make #1 fail closed instead of silently.

3. MEDIUM — any unparseable-timestamp row anywhere in the CSV invalidates the whole measurement (utils/aggregate_power.py:387)

invalid_timestamp_sample is appended before the window filter (necessarily — the row can't be located in time), so a transient monitor error line during warmup/eval (nvidia-smi occasionally emits Unable to determine… lines; amd-smi schema hiccups) zeroes power_valid for an otherwise sound window — and fails the job under REQUIRE_POWER=1. This contradicts the comment a few lines below ("Bad warmup/eval rows farther away must not invalidate an otherwise sound measurement"), which only holds for power/GPU-id parse failures. Strict-is-safe is defensible, but this is your most likely canary flake source. Consider recording an unparseable_row_count in the sidecar and only invalidating rows that could plausibly touch the window, or at minimum align the comment with the actual behavior.

4. LOW — record_power_internal_error loses the sidecar if the agg JSON is unreadable (utils/process_result.py:54-102)

The agg patch and the validation-sidecar write share one try; if agg_result.read_text()/json.loads throws, the sidecar (with the internal_error details) is never written — the audit trail disappears exactly when things went wrong. Write the sidecar first, or split into two try blocks.

5. LOW — legacy aggregate_power() / _load_bench_window() / patch_agg_result() are now dead code (utils/aggregate_power.py:154)

The docstring says "retained for callers of the pre-PR1 helper", but the only production caller repo-wide is process_result.py, which now uses run()/integrate_power(). Keeping a "must not be used for energy calculations" API around invites exactly that misuse — I'd delete them (and their tests) or clearly schedule the removal.

6. Notes / questions (no change required)

  • test-process-result.yml: removing if: github.event.pull_request.draft != true means the CPU tests now run on all draft PRs — intentional? (It does help your draft-canary flow, just confirming.)
  • Multinode + REQUIRE_POWER=1 hard-fails with no sidecar written (utils/process_result.py:282-289). Scope-consistent, but if REQUIRE_POWER ever gets set at a sweep level rather than per-job, every multinode job dies post-artifacts. Worth a comment in the workflow input description.
  • Power audit bundle uses if-no-files-found: ignore — under a strict canary, warn would surface a silently-missing sidecar in the bundle. Also note it duplicates gpu_metrics.csv/agg_*.json already uploaded separately (intentional per the description, fine).
  • Timezone: naive nvidia-smi timestamps are parsed as local time against epoch benchmark times (pre-existing). A TZ mismatch fails closed via benchmark_window_not_bracketed rather than corrupting — right failure mode, just be aware when reading canary sidecar output.
  • num_gpus = TP × PP × PCP excludes DCP; if a DCP>1 deployment ever adds devices, the mismatch surfaces as expected_gpu_count_mismatch (fail-closed) rather than wrong math — acceptable.

What I verified locally

  • python -m pytest utils/test_aggregate_power.py utils/test_process_result.py -q80 passed
  • mypy utils/aggregate_power.py → clean
  • Trapezoidal/boundary-interpolation arithmetic in the tests checked by hand
  • The finding-[NVIDIA] Add TRT-LLM 70B FP8 via slurm #1 truncation/concatenation parse reproduced with csv.DictReader

中文:本 PR 整体质量很高——按 GPU 的梯形积分与边界插值在数学上正确,校验原因码可审计,产物写入为原子操作,REQUIRE_POWER 的严格/尽力模式分层清晰,测试覆盖充分。主要发现:(1) stop_gpu_monitor 在 kill 掉后台 nvidia-smi 后追加最终采样时,若原文件末行被截断(无换行符),追加行会与残行拼接,_parse_power 会从拼接单元格中提取出貌似合理的功率值(如 52026 W),静默污染窗口末端的能耗积分而非置 power_valid=0——建议追加前检测并删除截断残行;(2) _parse_power 无合理性上下限,建议增加单卡功率合理区间校验;(3) :CSV 中任意位置的时间戳解析失败都会使整个测量失效(与代码注释矛盾),在 REQUIRE_POWER=1 下是最可能的 canary 偶发失败来源;(4) record_power_internal_error 中 agg JSON 读取失败会导致审计 sidecar 也不写出,建议拆分 try 块;(5) :旧版 aggregate_power() 等函数已无生产调用方,建议删除。本地验证:80 项测试通过,mypy 无问题,发现 #1 已用 csv.DictReader 实际复现。

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

Beyond the inline nit, three other candidate issues were examined this run and ruled out: an unparseable timestamp mid-CSV doesn't invalidate the whole run (by design — only power/GPU-id values outside tolerance do; timestamp parse failures are dropped per-row); multinode + REQUIRE_POWER=1 fails the job without writing a power_validation sidecar (multinode telemetry is explicitly out of scope for this PR, so no sidecar is expected there); and the power metric key list is duplicated between aggregate_power.py's _POWER_METRIC_KEYS and process_result.py's record_power_internal_error (cosmetic duplication, not a correctness issue).

Extended reasoning...

This PR is large and touches benchmark-result-affecting logic (new power integration/validation algorithm, fail-closed REQUIRE_POWER semantics, and several CI workflow changes), so it warrants human review regardless of the minor docstring nit already flagged inline. The three items above were raised by finder agents and investigated this run; none turned out to be bugs, but recording them here avoids re-litigating the same questions in a future pass.

Comment thread utils/aggregate_power.py
Comment on lines +236 to +260
def _append_reason(reasons: list[str], reason: str) -> None:
if reason not in reasons:
reasons.append(reason)


def _gpu_sort_key(gpu_id: str) -> tuple[int, int | str]:
return (0, int(gpu_id)) if gpu_id.isdigit() else (1, gpu_id)


def _empty_integration(
*,
expected_num_gpus: int | None,
reasons: list[str],
) -> PowerIntegration:
return PowerIntegration(
power_valid=False,
invalid_reasons=tuple(reasons),
expected_num_gpus=expected_num_gpus,
observed_gpu_ids=(),
per_gpu_sample_counts={},
per_gpu_max_sample_gap_s={},
per_gpu_energy_j={},
device_issues={},
)

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.

🟡 Five new private helpers in utils/aggregate_power.py (_append_reason, _gpu_sort_key, _empty_integration, _write_json_atomic, _validation_payload) lack docstrings, even though AGENTS.md's Python conventions call for docstrings on functions and every other function in this file (pre-existing and newly added) has one. Not blocking — just add one-line docstrings to these five for consistency.

Extended reasoning...

What the bug is. AGENTS.md's "Code Conventions" section (line 76) explicitly lists "docstrings on functions" as a Python convention for this repo. utils/aggregate_power.py follows this convention consistently everywhere else: every pre-existing function, including private helpers like _parse_timestamp, _parse_power, and _detect_columns, carries a docstring, and the majority of new functions this PR adds (_interpolate_power, _integrate_device, integrate_power, _load_benchmark_data, _derived_metrics, run) do too. However, five new private helpers introduced by this PR have no docstring at all — their bodies start directly with code:

  • _append_reason (~line 236)
  • _gpu_sort_key (~line 240)
  • _empty_integration (~line 244)
  • _write_json_atomic (~line 607)
  • _validation_payload (~line 671)

Where this shows up in the diff. All five are net-new additions in this PR's diff to utils/aggregate_power.py — they did not exist before and so cannot be attributed to prior technical debt. Each is genuinely a fresh omission introduced alongside sibling functions in the same PR that do follow the convention, which is what makes this a real (if narrow) inconsistency rather than pre-existing drift.

Why existing tooling does not catch this. The PR description states Ruff and mypy both passed. Neither tool enforces docstring presence by default (that would require enabling a plugin like pydocstyle/ruff --select D, which this repo does not appear to have configured), so a missing docstring on a private helper is silent at both lint and type-check time. Nothing else in CI (the pytest suite, workflow contract tests) exercises documentation completeness either.

Impact. Purely cosmetic — none of the five helpers has any runtime, correctness, or behavioral effect from missing a docstring. They are also short and largely self-explanatory from their names and one-line bodies (e.g. _gpu_sort_key returns a sort key tuple; _write_json_atomic writes JSON to a temp file and renames it). This does not affect merge safety.

Proof (concrete diff excerpt). Compare a passing case to a failing case within the same PR diff:

def _interpolate_power(samples: list[tuple[float, float]], timestamp: float) -> float:
    """Linearly interpolate power at a timestamp bracketed by ``samples``."""
    ...

def _gpu_sort_key(gpu_id: str) -> tuple[int, int | str]:
    return (0, int(gpu_id)) if gpu_id.isdigit() else (1, gpu_id)

_interpolate_power (also new in this PR) has a docstring immediately following the def line; _gpu_sort_key (also new in this PR, defined a few lines earlier at ~line 240) jumps straight to its return statement. The same asymmetry repeats for _append_reason, _empty_integration, _write_json_atomic, and _validation_payload versus their well-documented siblings _integrate_device, integrate_power, _load_benchmark_data, and _derived_metrics.

Suggested fix. Add a one-line docstring to each of the five helpers describing its purpose/return value, matching the style used elsewhere in the file (e.g. """Return a sort key that orders numeric GPU IDs before non-numeric ones.""" for _gpu_sort_key). This is a few minutes of work and does not require any behavioral change.

Remove an incomplete trailing nvidia-smi CSV row before appending the deterministic post-benchmark sample, preventing row concatenation from silently corrupting energy integration. Add a shell-lifecycle regression test covering the interrupted-write case.

中文:在追加确定性的基准测试后采样前,删除未写完整的 nvidia-smi CSV 末行,避免行拼接静默污染能耗积分;新增覆盖中断写入场景的 shell 生命周期回归测试。

Copy link
Copy Markdown
Collaborator Author

Superseded by #2323. I reopened this from an upstream branch so repository CI, automated review, and the H100/H200 canary can run with the required permissions. The same implementation history, including the truncated NVIDIA telemetry-row fix, is preserved in the replacement PR.

中文:本 PR 已由 #2323 替代。新的 PR 从 upstream 分支提交,使仓库 CI、自动代码审查和 H100/H200 canary 能够获得所需权限并正常运行;原有实现历史及 NVIDIA 遥测残行修复均已保留。

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants