Skip to content

feat(observability): drop the aiperf/tachometer scrape exclusion - #396

Merged
nv-yna merged 1 commit into
NVIDIA:mainfrom
nv-yna:feat/tachometer-drop-client-complement
Sep 7, 2026
Merged

nv-yna merged 1 commit into
NVIDIA:mainfrom
nv-yna:feat/tachometer-drop-client-complement

Conversation

@nv-yna

@nv-yna nv-yna commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

What

Removes the complementary-scrape rule between the benchmark client and tachometer: tachometer now scrapes every configured endpoint, even when AIPERF_SERVER_METRICS_URLS points the client at the same URL.

  • generate_tachometer_config loses its exclude_urls parameter and the skip inside the backend-endpoint loop
  • TelemetryStageMixin.start_tachometer no longer derives the client-polled set
  • BenchmarkStageMixin._client_polled_metric_urls deleted (its only consumer was the exclusion)
  • exclusion test replaced by test_client_polled_urls_are_still_scraped

Why

Double-polling a worker /metrics endpoint has been validated as harmless, and the complement rule had a real cost: whichever endpoints the client claimed disappeared from the whole-window tachometer capture, so per-replica engine series went missing exactly when a benchmark was running — the one window the capture exists for.

This also simplifies the ground for a follow-up PR that adds trtllm-serve-mode scrape targets.

Testing

make check — 1686 passed, 2 skipped.

🤖 Generated with Claude Code

Tachometer used to scrape the complement of AIPERF_SERVER_METRICS_URLS so
a worker /metrics endpoint was never polled by both the benchmark client
and the in-job scraper. Double-polling has since been validated as
harmless, and the complement rule had a real cost: whichever endpoints the
client claimed disappeared from the whole-window tachometer capture, so
per-replica engine series were missing exactly when a benchmark ran.

Remove the exclusion end to end: generate_tachometer_config loses its
exclude_urls parameter, start_tachometer no longer derives the client set,
and BenchmarkStageMixin._client_polled_metric_urls (whose only consumer
was that exclusion) is deleted. Tachometer now scrapes every configured
endpoint unconditionally.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@6466033). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #396   +/-   ##
=======================================
  Coverage        ?   73.44%           
=======================================
  Files           ?      101           
  Lines           ?    14032           
  Branches        ?        0           
=======================================
  Hits            ?    10306           
  Misses          ?     3726           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nv-yna
nv-yna merged commit f098b84 into NVIDIA:main Sep 7, 2026
6 checks passed
FrankD412 added a commit to FrankD412/srt-slurm that referenced this pull request Sep 10, 2026
Commit 7640839 ported this test from ajc/rust-cpu-exporter, but upstream
dropped the aiperf/tachometer scrape exclusion in NVIDIA#396, so
generate_tachometer_config no longer accepts exclude_urls and the test
failed with a TypeError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
FrankD412 added a commit that referenced this pull request Sep 10, 2026
…-per-watt report (#410)

* feat(telemetry): collect host CPU power

* fix(sa-bench): publish power measurement windows

* feat: record local wall-clock time in CPU power samples

Adds a timestamp_local column (ISO 8601 with UTC offset) to the CPU
power telemetry CSV, derived from timestamp_unix. Lets an offline
consumer read a node's real UTC offset directly from the samples file
instead of guessing the cluster's timezone when correlating against
benchmark logs that only carry local time-of-day text.

Bumps schema_version 1 -> 2; cpu_power_session.py's merge logic is
updated for the shifted column indices.

Signed-off-by: Frank Di Natale <3429989+FrankD412@users.noreply.github.com>

* feat: add CPU/GPU trapezoidal energy-per-token report

Adds srtctl.analysis.power_energy_report, a standalone module that
joins a run's CPU (power/cpu/samples.csv) and GPU (power/samples.csv)
power telemetry against each concurrency point's profiling window and
token counts, integrating power into energy with numpy.trapezoid.

Timestamps are read directly from existing wall-clock sources rather
than reconstructed: aiperf's profile_export.jsonl already carries
time.time_ns() per record, and sa-bench's result JSON already carries
benchmark_start_time_unix/benchmark_end_time_unix (mtime-based
derivation was tried and found unreliable once run directories get
copied/archived). GPU energy is broken down per-device, per-node, and
per-role using the topology already recorded in power/manifest.json.

Wires this into run_postprocess (postprocess_stage.py) as a best-effort
step alongside the perf dashboard build, writing
<log_dir>/power_energy_report.json on every job where CPU/GPU
telemetry and a supported benchmark type are present; quietly skipped
otherwise.

Signed-off-by: Frank Di Natale <3429989+FrankD412@users.noreply.github.com>

* feat(telemetry): collect detailed hwmon power domains

* fix(telemetry): map Grace hwmon power domains

* feat(telemetry): support agentic/agentx/custom benchmark types

telemetry validation previously required benchmark.type: sa-bench,
rejecting agentic, agentx, and custom benchmark runs outright.

* feat(cpu-power): port Rust cpu-power-exporter crate onto Kyle's branch

Ships the statically-linked Rust binary (aarch64/x86_64 musl) that reads
/sys/class/hwmon hwmon*/power*_average and serves cpu_power_acpi_watts
on :9405/metrics for AIPerf --server-metrics scraping.

Covers:
- src/cpu-power-exporter/ Rust crate (tokio, tracing, clap, anyhow)
- Workspace member added to Cargo.toml
- docker/Dockerfile.cpu-power-exporter (musl static, two-arch)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* feat(cpu-power): add two-arch CI matrix and make targets for cpu-power-exporter

- Makefile: cpu-power-exporter / cpu-power-exporter-download targets,
  CPU_POWER_EXPORTER_RELEASE var, setup now depends on both downloads
- release.yaml: unified change detection (single step, two outputs),
  build-cpu-power-exporter job (linux/amd64 + linux/arm64),
  graceful carry-forward for both binaries from the previous release,
  nullglob-safe gh release create

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* feat(cpu-power): wire Rust exporter into telemetry launch and AIPerf metrics

- telemetry_stage: add _resolve_bundled_binary (generalized from
  _resolve_tachometer_binary), add _start_cpu_power_prometheus_exporters
  which launches bin/cpu-power-exporter via srun on each worker node,
  call it from start_cpu_power_telemetry when prometheus_port > 0,
  fix dcgm_exporter guard (return None instead of raise when None)
- benchmark_stage: inject cpu-power-exporter node URLs into
  AIPERF_SERVER_METRICS_URLS when cpu_power.prometheus_port > 0
- submit.py: validate_setup checks bin/cpu-power-exporter alongside tachometer
- schema.py: add CpuPowerConfig.prometheus_port (default 9405),
  gate _validate_dcgm_power on dcgm_exporter is not None

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* chore(cpu-power): add test recipe for lyris GB200 cpu-power-exporter e2e

Uses trace-replay benchmark type so AIPerf actually consumes the
AIPERF_SERVER_METRICS_URLS endpoints. Qwen3.5-27B BF16 to avoid the
8-minute FP4 JIT autotuning pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* feat(telemetry): add stdlib Python Prometheus exporter for Grace CPU power

Ports cpu_power_exporter.py from kylliang/power_study_20260901 (commit 2a13cb1).
Default port updated to 9405 (consistent with the rest of the cpu-power stack).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* feat(telemetry): fall back to Python exporter when Rust cpu-power-exporter is absent

If the bundled Rust binary is not present or not executable (e.g. x86_64 nodes
or a checkout without a pre-built bin/), _start_cpu_power_prometheus_exporters
now falls back to `python3 -m srtctl.core.cpu_power_exporter` so Prometheus
scraping still works everywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* feat(cpu-power-v2): port schema validation, IPv6 url_host, validate_setup, and tests from ajc/rust-cpu-exporter

- Add url_host() to ip_utils for IPv6 URL bracket safety
- Use url_host() in benchmark_stage.py CPU power URL construction
- Add arch_from_binary() to dynamo_wheels.py
- Rewrite CpuPowerConfig: drop 'dcgm' source, add storage_subdir, acpi_mandatory
- Add _dynamo_system_ports(), _validate_collector_budget(), _validate_cpu_power(),
  _reject_inert_cpu_power_demand() to schema validation
- Rewrite _validate_telemetry() to support CPU-only, DCGM-only, or combined modes
- Fix _validate_observability() to not conflict CPU-only telemetry with Tachometer DCGM
- Improve telemetry_stage.py: drop permitted_device_keys, use cpu_power.storage_subdir
- Update Makefile: version marker, arch check, warn-on-failure for latest release
- Add cpu-power-exporter-setup target; switch setup to use it
- Update validate_setup() to conditionally require cpu-power-exporter
- Add _cpu_power_exporter_problem() with executability and arch mismatch detection
- Port IPv6 tachometer tests and cpu-only telemetry stage test from rust-cpu-exporter
- Port validate_setup tests: disabled/enabled/arch-mismatch/non-executable cases

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* ci: add cpu-power-exporter.yaml workflow for PR and push validation

Mirrors tachometer.yaml: runs cargo fmt/test on source changes, and
a two-arch Docker cross-compile to validate the release Dockerfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>

* fix(tests): repair test function name mangled by cherry-pick auto-merge

git cherry-pick of e11d928 concatenated the new test's def line with
the pre-existing one during auto-merge, breaking the file's syntax.

* fix(cpu-power-exporter): declare tracing workspace deps

Cargo.toml already referenced tracing/tracing-subscriber via
workspace = true, but the workspace root never declared them.

* feat(cpu-power): add cpu/samples.csv wire-format constants

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(cpu-power): add cpu/samples.csv row, writer, and reader

* feat(cpu-power): parse dcgm/acpi cpu-power-exporter scrapes

Implement CpuReading, ParsedCpuScrape, and parse_cpu_scrape to parse
Prometheus-format /metrics text from the cpu-power-exporter Rust binary.
Supports both DCGM (one aggregated value per socket) and ACPI (per-channel
detail: cpu/sysio/grace) modes, with ACPI preferred when both present.

In ACPI mode, total_power_w sums only grace-kind channels, reflecting the
hardware's actual power boundary measurement for Grace SoC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(cpu-power): add best-effort CpuPowerCollector

* refactor(power): replace host-side CPU power system with best-effort head-node scraper

This branch had two CPU power telemetry implementations: an older
host-side collector (src/srtctl/core/cpu_power.py,
cpu_power_session.py, TelemetryConfig.cpu_power) that runs on every
worker node and reads ACPI/DCGM sysfs directly, and a newer
head-node scraper (src/srtctl/core/power/cpu_session.py,
CpuPowerCollector) that polls each worker's cpu-power-exporter
/metrics endpoint from the head node, matching the DCGM power leg's
architecture.

Keep the newer collector, delete the older system, and repoint the
two things worth keeping from it:
  - AIPerf's AIPERF_SERVER_METRICS_URLS injection (benchmark_stage.py)
  - the submit-time preflight check for the bundled cpu-power-exporter
    binary (submit.py)

both now read the new, much smaller TelemetryConfig.cpu_power_exporter
(presence-gated, not a separate enabled flag) instead of the removed
CpuPowerConfig. telemetry_stage.py gains start_cpu_power_telemetry /
finalize_cpu_power_telemetry methods that launch the exporter with the
same bundled-binary-with-Python-fallback pattern the old code used,
but on the bare host (no container) and fully best-effort: launch
failures are absorbed and CPU power never mutates the job's exit code.

Also fixes a MagicMock-config test in test_power_collector.py that
started reaching real endpoint-allocation code once
start_cpu_power_telemetry stopped early-returning on a mocked
telemetry.cpu_power.enabled.

* test(dry-run): show cpu_power_exporter port in submit dry-run details

Mirrors the existing dcgm_exporter row; cpu_power_exporter has no
container_image so the row is just the port.

* fix(power): restore standalone cpu-power-only telemetry mode; fix submit.py preflight gate

- schema.py: _validate_telemetry now only raises when both dcgm_exporter and
  cpu_power_exporter are None, restoring standalone CPU-power-only telemetry.
  _validate_cpu_power_exporter() runs unconditionally when telemetry is enabled.
- submit.py: preflight cpu_power_enabled gate now also requires
  config.telemetry.enabled, not just cpu_power_exporter being set.
- telemetry_stage.py: start_tachometer's dcgm exporter sharing check now
  requires telemetry.dcgm_exporter to actually be configured, not just
  telemetry.enabled -- otherwise a CPU-power-only telemetry config silently
  suppressed the tachometer's own dcgm_exporter with no exporter running at all.
- tests: add coverage for CPU-power-only telemetry (schema + validate_setup),
  and restore test_cpu_only_telemetry_leaves_tachometers_dcgm_exporter_running
  adapted to the new CpuPowerExporterConfig field.

* fix(power): restore CPU power exporter port-collision validation; fix blank-total crash

- schema.py: _validate_cpu_power_exporter now checks source is one of
  auto/acpi/dcgm, and rejects a port colliding with telemetry.dcgm_exporter,
  observability.tachometer.{dcgm,node}_exporter, or a Dynamo system port
  (via the previously-unused _dynamo_system_ports helper). Ported from the
  deleted _validate_cpu_power validator.
- power_energy_report.py: load_cpu_samples now skips rows with a blank
  total_power_w instead of crashing with ValueError(float("")) -- blank is
  the documented normal case for an ACPI scrape with no grace channel.

* fix(power): add source passthrough, fix stale recipe, and dry-run/het-launch bugs

- schema.py: add CpuPowerExporterConfig.source (auto|acpi|dcgm), mirroring
  the Rust cpu-power-exporter binary's own --source flag; validated in
  _validate_cpu_power_exporter.
- telemetry_stage.py: pass --source through to the bundled Rust binary;
  warn (not silently drop) when a non-auto source is requested but the
  ACPI-only Python fallback exporter is used instead. Also move the
  het-group resolution block inside the try/except so an unresolvable node
  is absorbed like any other launch failure, per the method's own
  best-effort docstring contract, instead of aborting the sweep.
- configs/cpu-power-test.yaml: fix stale telemetry.cpu_power block (no
  longer loads -- "Unknown field") to the current telemetry.cpu_power_exporter
  shape, dropping the removed `required` flag and preserving `source: acpi`.
- submit.py: move the cpu_power_exporter dry-run row inside the
  `telemetry.enabled` guard so it doesn't display a port that will never
  actually launch.
- Tests: new TestCpuPowerExporterConfig source/collision cases, new
  TestCpuPowerExporterLaunch source-passthrough/fallback-warning/het-absorb
  cases, an explicit configs/cpu-power-test.yaml load test (configs/ isn't
  covered by the recipes/**/*.yaml glob and holds non-recipe files, so a
  narrow explicit test is safer than widening that glob), and a dry-run
  test confirming the row is hidden when telemetry is disabled.

* docs(power): rewrite CPU power docs for the current cpu_power_exporter system

- config-reference.md: replace the deleted telemetry.cpu_power block
  (BTK ordering, per-rail sensor names, sum-based total) with
  telemetry.cpu_power_exporter (port, source), the grace-rail-is-the-total
  convention, and best-effort/no-required semantics.
- cpu-power-telemetry.md: full rewrite describing the current
  CpuPowerCollector head-node HTTP-scrape design, cpu/samples.csv format
  (including the blank-total-without-grace caveat), and the bundled
  Rust-binary-with-Python-fallback launch model. The old doc described a
  fully deleted system (srtctl.core.cpu_power, CpuPowerConfig,
  wait_for_readiness/.ready.json, per-node CSV merging, embedded-mode DCGM
  bindings) that no longer exists.
- dcgm-4.7-runtime-support.md: fix the stale telemetry.cpu_power reference
  to telemetry.cpu_power_exporter.

* style(power): apply ruff format to submit.py dry-run row

Reformats the cpu_power_exporter dry-run row (added in the previous commit)
to fit the 120-char line limit per ruff format.

* style(power): fix ruff lint (import order, RUF012 ClassVar)

* refactor(power): split sample loaders into path and handle variants

* feat(power): add tolerant CSV reader for live samples.csv

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(power): add IncrementalPowerEmitter for per-case energy emission

* feat(power): add IncrementalPowerWatcher daemon thread

* feat(power): wire incremental power report into the postprocess mixin

* feat(power): wire incremental power report into the sweep lifecycle

* test(power): exercise multi-case emit loop and deterministic final-poll assertion

_make_sa_bench_log_dir only ever built a single concurrency point, so the
per-case emit loop in IncrementalPowerEmitter.poll() was never driven with
more than one pending case -- a bug that dropped or over-added an unready
case would have passed the whole suite. Generalize the fixture to accept
multiple (concurrency, start, end) cases and add a test where samples
bracket only the first of two.

test_watcher_runs_a_final_poll_on_stop's ">= 1" assertion is a tautology:
the loop's own first poll already satisfies it even if stop_and_finalize's
final pass were deleted. Add a deterministic, timing-independent assertion
in test_stop_without_start_is_safe (where self._thread is None, so the
final pass is the ONLY possible poll) and note why in the timing-based test.

* fix(power): mark permanently-uncovered cases dead instead of re-scanning forever

A case that can never become ready (e.g. the collector died before the
window closed, so the end-gap never shrinks) previously stayed in pending
forever: every tick paid a full benchmark.out scan, three rglobs, and a
read+parse+numpy build of samples.csv -- possibly O(100MB) -- in the daemon
thread, at DEBUG-only visibility. Since samples only grow forward in time,
"permanently unready" is decidable: once the newest sample seen is already
past window.end_unix + MAX_SAMPLE_GAP_SECONDS, no future sample can shrink
that gap. Track such cases like _emitted so later polls skip them, and log
the reason once at WARNING.

Also key _emitted (and the new _dead set) on source path rather than
concurrency alone: discover_run's sources come from an rglob, so two
sources could in principle share a concurrency (nested run dirs, an
archived run copied under log_dir, a future multi-ISL layout), and the
second would previously have been silently dropped.

* fix(power): finalize incremental power report after node teardown, not before

finalize_incremental_power_report() previously ran before stop_event.set()
/ registry.cleanup() / _run_host_teardown() in do_sweep.py's cleanup block.
Its final pass plus up to a DEFAULT_JOIN_TIMEOUT_SECONDS thread join
therefore delayed node-state reversion -- bad on the SLURM walltime-kill
path this feature exists to survive, where a fixed grace clock is running.

Move the call to immediately before run_postprocess(), after
_run_host_teardown(). The ordering requirement that it must still run after
finalize_power_telemetry/finalize_cpu_power_telemetry (so it reads closed,
durable CSVs) continues to hold since both already ran earlier in the same
block. Also tighten DEFAULT_JOIN_TIMEOUT_SECONDS from 30.0 to 10.0 so the
worst case added to teardown is smaller.

* docs(power): note the incremental index is idempotent per-process only

The module docstring said "idempotent" without qualifying that the
guarantee only holds within a single process's lifetime: the co-located
power_energy_c<N>.json file is overwrite-idempotent, but
power_energy_report.jsonl is append-only against an in-memory set, so a
job requeued into the same log dir appends duplicate concurrency rows.
Document that consumers should dedupe by concurrency, last-wins by
emitted_at_unix (index_path's own docstring already says this, added
alongside the dead-case-detection commit).

* feat(power): add utilization metric constants and v2 samples header

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(power): parse optional GPU utilization alongside power

* feat(power): persist optional utilization columns; read v1 and v2 samples

* feat(power): forward parsed utilization into persisted sample rows

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(power): advertise samples schema version and utilization columns in manifest

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(power): document UTILIZATION_METRICS coupling; assert validity with utilization present

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(power): finish the collect_interval_ms migration for the CPU power leg

Upstream #401 retired telemetry.default_frequency in favour of
collect_interval_ms. The DCGM leg picked that up in the rebase; the CPU
power collector still read the removed field and raised AttributeError at
launch. Derive its sample interval the same way the DCGM leg does, move
the dry-run and telemetry fixtures to the new knob, expect the built-in
node exporter that #358 now launches by default, and drop the retired
name from the CPU power docs.

* fix(power): check CPU exporter port collisions against the resolved tachometer exporters

Upstream #358 makes the tachometer launch built-in DCGM (9401) and node
(9101) exporters when no explicit block is configured. The CPU power
exporter's collision check compared against the raw fields, which are None
in that case, so a cpu_power_exporter on 9401 passed validation and
collided at runtime. Compare against resolved_dcgm_exporter /
resolved_node_exporter and cover both defaults.

* feat(power): restore the host-side Python CPU power collector as an independent leg

Commit 0975241 replaced the in-job host collector (srtctl.core.cpu_power,
cpu_power_session, TelemetryConfig.cpu_power) with the head-node scraper
over cpu-power-exporter. The scraper stays, but the host collector is
needed as an alternative: it has fail-closed `required` semantics, needs
no exporter binary or reserved port, and reads ACPI/DCGM directly on each
node.

Bring back cpu_power.py, cpu_power_session.py, and tests/test_cpu_power.py
from the tree just before the cleanup (the newest version, with the Grace
socket-total domain mapping and timestamp_local). Reinstate
`telemetry.cpu_power` in Kyle Liang's original shape -- enabled, source
(auto|acpi|dcgm), sample_interval_seconds, startup_timeout_seconds,
required, storage_subdir -- without the `prometheus_port` field that was
later bolted on: the exporter and its AIPerf URL injection now belong to
`telemetry.cpu_power_exporter`, so the two blocks share no ports and no
output directories and may be enabled independently or together.

The stage gains start_cpu_power_host_telemetry / finalize_cpu_power_host_
telemetry alongside the scraper's methods, power_telemetry_blocks_benchmark
honours cpu_power.required again, do_sweep calls both legs, and dry-run
shows the block. power_energy_report is deliberately untouched: aligning
this leg's CSV header and location with the scraper (and adding
utilization) is follow-on work, noted in docs/cpu-power-telemetry.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(power): collect CPU utilization in the host collector and report utilization per window

The host-side collector (telemetry.cpu_power) now watches DCGM CPU entity
fields 1100-1104 (total/user/nice/sys/irq) in the same field group as power
field 1130 and reads them in the same latest-values call. Five trailing
columns join every per-socket sample row (cpu_util_total, cpu_util_user,
cpu_util_nice, cpu_util_sys, cpu_util_irq); the samples schema moves from
v2 to v3. ACPI has no utilization, so those cells stay blank there, and the
per-node metadata lists the field ids and the DCGM unit (fraction of socket
CPU time). The GPU samples.csv is untouched: gpu_util_pct and sm_active
already live there.

The energy report gains utilization next to the joules. It reads the CPU
and GPU utilization columns by name (absent columns and blank cells are
skipped, so v1/v2 files still load), and for each concurrency window
reports the mean and max of the samples inside the window per CPU socket
and node, and per GPU, node, and role. Utilization is a gauge, so it is
summarized rather than integrated, and a window with no utilization
samples is a warning rather than an error. Discovery now also accepts the
host collector's cpu_power/samples.csv (previously misclassified as a GPU
file); when a run has both CPU legs, --cpu-samples picks one.

_sorted_series becomes generic in its key type, which clears the
long-standing invariance errors ty reported in this module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(power): report perf/W and a reported-vs-computed timing comparison per window

Each concurrency window in the energy report now carries perf per watt:
output and total tokens/s over the computed window, GPU average watts and
CPU+GPU combined average watts (joules / duration), and the four tokens/s
per watt ratios. These are the reciprocal of the existing J/token figures
and are derived from the same window, so the two can never disagree. The
combined variants are None, with a warning, unless both a CPU and a GPU
leg produced samples, so they never silently equal the GPU-only number.

Alongside, three timelines for comparison (never validation):
  computed  -- the window the trapezoid integrates over, as before
  reported  -- the benchmark's own account: sa-bench's `duration` and
               wall-clock start/end; aiperf's aggregate benchmark_duration
               and start_time/end_time; failing that, the profiling-phase
               NOTICE lines in benchmark.out when exactly one phase ran
               (time-of-day only, so duration without absolute start/end)
  coverage  -- the first/last power samples actually spanned, and every
               breakdown row records its own sample_start/end and count

Text output gains `timing:` and `perf/W:` lines; JSON gains `timing` and
`perf_per_watt` objects and per-breakdown sample fields. The incremental
during-job report inherits all of it through report_to_dict.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(power): add sample-based power percentiles to every energy breakdown row

Each breakdown row (socket, GPU, node, role) now carries a distribution of
the power samples the trapezoid actually spanned: mean_w, min_w, p5_w,
p50_w, p95_w, p99_w, max_w. The existing avg_power_w stays as the
time-weighted average (joules / duration), and mean_w is kept next to it
so any gap between the two under uneven sampling is visible rather than
hidden. Node and role rows are computed over their summed series, so a
node p99 is the 99th percentile of the node's total power, not a sum of
per-device percentiles.

The text table appends p50/p95/p99/max to each breakdown line; JSON gets
all seven fields, and the incremental report inherits them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(power): align Rust CPU power exporter domain naming with Python collector

The ACPI OEM-string naming for CPU power rails varies by platform (e.g.
"Grace Power Socket N" vs. a generic "Total Power socket N", some suffixed
with "in uW"). Kyle's host-side Python collector already accounted for all
these variants; the Rust exporter only recognized 3 literal substrings and
was missing the "Total Power" spelling, the "in uW" suffix, and the
CPU-rail/SoC-rail/DRAM domains entirely, silently dropping total_power_w on
platforms that don't report a "Grace" rail.

Renames the exporter's type= taxonomy to match Python's semantic kinds
(total/cpu_rail/soc/dram) and updates the downstream Prometheus scrape
parser to sum on kind=="total" instead of kind=="grace".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(cpu-power): apply cargo fmt to the exporter crate

Two commits landed without running rustfmt, so both Rust workflows fail
at `cargo fmt --all --check` on six over-width lines in dcgm.rs and
main.rs. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cpu-power): drop the collector-thread test the ported crate no longer has

The wedged-collector test and its serve() helper reference Collector,
CollectRequest, mpsc and thread, none of which exist in this crate: the
port replaced the request/reply collector thread with a background poller
that fills an RwLock cache, so a scrape never touches sysfs and the 503
timeout path the test asserted on is gone. The test binary therefore
failed to compile, which CI never reached because rustfmt failed first.

Also removes the now-unused COLLECT_TIMEOUT constant and oneshot import.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build(cpu-power): install libc6-dev-arm64-cross for the arm64 static link

The arm64 release build fails with `ld: cannot find -ldl`. libloading
emits `-ldl` on every linux target, and a static link needs libdl.a, but
`--no-install-recommends gcc-aarch64-linux-gnu` only pulls the glibc
runtime cross package (libc6-arm64-cross); the dev package that ships
/usr/aarch64-linux-gnu/lib/libdl.a is a Recommends. The amd64 build was
unaffected because the base image already carries the host libc6-dev.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(telemetry): drop the exclude_urls test for a feature main removed

Commit 7640839 ported this test from ajc/rust-cpu-exporter, but upstream
dropped the aiperf/tachometer scrape exclusion in #396, so
generate_tachometer_config no longer accepts exclude_urls and the test
failed with a TypeError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(telemetry): bracket IPv6 worker and frontend hosts in tachometer URLs

get_hostname_ip can return an IPv6 literal, and the backend and frontend
scrape targets interpolated it bare, so the colons inside the address were
read as the port separator. Route both through url_host(), which the
benchmark stage already uses for the cpu-power-exporter targets.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Signed-off-by: Frank Di Natale <3429989+FrankD412@users.noreply.github.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Co-authored-by: Kyle Liang <kylliang@nvidia.com>
Co-authored-by: Anthony Casagrande <acasagrande@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants