jouleclaw: the NVML sampling floor is ~100 ms, measured, not ~50 ms #316

Merged
dcharlot merged 1 commit from fix/jouleclaw-nvml-sampling-floor into main 2026-10-18 13:44:41 -04:00
Owner

Found by a claim-falsification sweep of the AI/energy literature, then re-read against the primary source before changing anything.

The falsified claim

jouleclaw/SPEC.md §1.3 published a normative floor:

- NVIDIA discrete (NVML cumulative-energy): ~1 mJ, ~50 ms. `HwShunt`.

feeding the MUST three lines above it — "Implementations MUST surface resolution_uj() and min_window_ns() so callers can refuse to claim microjoule accuracy on platforms that cannot deliver it." min_window_ns() returned 50_000_000, and the figure was restated at nvml.rs:32, :46, :279 and lib.rs:417.

Dauner et al., Evaluating the Influence of Measurement Frequency on Energy Readings Using Intel RAPL and NVIDIA NVML, ACM SIGENERGY Energy Informatics Review 6(2), July 2026 (HotCarbon), measured it across 588 controlled runs:

"The NVML cumulative-energy counter does not update faster than roughly 100 ms on any device tested."

Stale-read fraction: 93.4 % at 0.5 ms, 86.3 % at 10 ms, 79.9 % at 20 ms, falling to 1.0 % at 100 ms and zero from 500 ms.

So 50 ms does not merely understate the floor — it sits inside the stale-read region, where a read mostly returns the preceding value. We graded that HwShunt, our strongest provenance. "A counter read below its own update rate is not a reading of the span" is the rule this family exists to enforce, and our own SPEC broke it.

Two numbers, kept apart on purpose

The paper reports a second, different quantity: accuracy against integrated power. That is device-class dependent — an A100 agreed to ±0.2 % at every interval, while an unvalidated RTX 4090 was still 13.3 % low at 100 ms and came within 1.6 % only at 1 s.

It would have been easy to fold that into min_window_ns() and return 1 s. I didn't, because:

  • min_window_ns is documented (lib.rs:416) as "the smallest sampling window below which readings are noise-dominated" — the counter's update period. That is exactly what the paper measures at ~100 ms.
  • It is load-bearing, not advisory: jouledesk-github's window_energy refuses a figure when the work is shorter than it.
  • Overloading one scalar with a second, device-dependent property would repeat the category error being fixed.

So the scalar moves to the measured update period, and the accuracy caveat goes into SPEC §1.3 prose together with the paper's own recommendation — on an unvalidated counter, prefer the power integral, which Mode::PowerIntegral already is, graded ModelBased.

Open question, flagged not decided: the honest NVML answer is arguably two numbers (validated data-center class vs unvalidated consumer class) and min_window_ns is one scalar. Either the backend reports a conservative 1 s until a device is validated against integrated power, or the trait grows a validation state. I've taken neither step here.

The sharper bug: the liveness probe could fail a healthy counter

PROBE_TICK was 100 ms, justified as "two of its ~50 ms sample periods". Against the real ~100 ms period the tick equalled the update period, so roughly half the intervals catch no update, the probe misses LIVE_SHARE (four in five) and grades a perfectly healthy counter not-live — silently demoting a genuine HwShunt reading to the power integral.

Now 150 ms, which matches sandbox-meter's NVML_PROBE_TICK (whose comment already carried the right derivation), and MIN_LIVE_ADVANCES (5) intervals then take 750 ms, inside the 1000 ms LIVENESS_BUDGET.

Nothing pinned either constant before. the_probe_tick_clears_the_update_period_and_fits_the_budget now pins both halves.

Falsification

mutation result
F1 min_window_ns back to 50_000_000 only nvml_resolution_is_millijoule_floor FAILED
F2 PROBE_TICK back to 100 ms (equal to the period) only the tick test FAILED — the "clears the period" half
F3 PROBE_TICK to 250 ms only the tick test FAILED — the "fits the budget" half

F2 and F3 hit opposite halves of the same test, so neither half is decoration.

Not changed

The RAPL floor. The same paper finds RAPL stable at 10 ms or coarser — which is what §1.3 already says. One caveat worth recording: that is validated stability, not validated accuracy against an external meter.

Verification

The nvml module is not in a default build, so a plain cargo test proves nothing here:

  • cargo test -p jouleclaw-energy --features rapl,nvml — 84 passed (CI's feature_tests for this row)
  • cargo check --workspace --all-targets + cargo test --workspace (jouleclaw-rs) — exit 0, 0 failed, 484 ok suites
  • cargo test -p mesh-meter --features hw-meter-nvml — 14 passed

That last one matters: jouleclaw-energy has five consumers outside jouleclaw/, and three (jouledesk-github, l3-jouleclaw, quantum-meter) pin default-features = false, features = ["ioreport"], so they never compile this module. Only mesh-meter's hw-meter-nvml reaches it — and CI's mesh row does not build that feature, so it would not have been caught. No test or doc anywhere pins the old 50 ms figure.

tests/nvml_hardware.rs ran 0 tests on the build host: it needs a real GPU and only exercises on the linux-hw runner.

🤖 Generated with Claude Code

Found by a claim-falsification sweep of the AI/energy literature, then re-read against the primary source before changing anything. ## The falsified claim `jouleclaw/SPEC.md` §1.3 published a normative floor: ``` - NVIDIA discrete (NVML cumulative-energy): ~1 mJ, ~50 ms. `HwShunt`. ``` feeding the **MUST** three lines above it — *"Implementations MUST surface `resolution_uj()` and `min_window_ns()` so callers can refuse to claim microjoule accuracy on platforms that cannot deliver it."* `min_window_ns()` returned `50_000_000`, and the figure was restated at `nvml.rs:32`, `:46`, `:279` and `lib.rs:417`. Dauner et al., *Evaluating the Influence of Measurement Frequency on Energy Readings Using Intel RAPL and NVIDIA NVML*, **ACM SIGENERGY Energy Informatics Review 6(2), July 2026** (HotCarbon), measured it across **588 controlled runs**: > "The NVML cumulative-energy counter does not update faster than roughly 100 ms on any device tested." Stale-read fraction: **93.4 % at 0.5 ms, 86.3 % at 10 ms, 79.9 % at 20 ms**, falling to 1.0 % at 100 ms and zero from 500 ms. So 50 ms does not merely understate the floor — **it sits inside the stale-read region**, where a read mostly returns the preceding value. We graded that `HwShunt`, our strongest provenance. "A counter read below its own update rate is not a reading of the span" is the rule this family exists to enforce, and our own SPEC broke it. ## Two numbers, kept apart on purpose The paper reports a second, different quantity: accuracy against integrated power. That is **device-class dependent** — an A100 agreed to ±0.2 % at every interval, while an unvalidated RTX 4090 was still **13.3 % low at 100 ms** and came within 1.6 % only at **1 s**. It would have been easy to fold that into `min_window_ns()` and return 1 s. I didn't, because: - `min_window_ns` is documented (`lib.rs:416`) as *"the smallest sampling window below which readings are noise-dominated"* — the counter's **update period**. That is exactly what the paper measures at ~100 ms. - It is **load-bearing, not advisory**: `jouledesk-github`'s `window_energy` refuses a figure when the work is shorter than it. - Overloading one scalar with a second, device-dependent property would repeat the category error being fixed. So the scalar moves to the measured update period, and the accuracy caveat goes into SPEC §1.3 prose together with the paper's own recommendation — on an unvalidated counter, prefer the power integral, which `Mode::PowerIntegral` already is, graded `ModelBased`. **Open question, flagged not decided:** the honest NVML answer is arguably two numbers (validated data-center class vs unvalidated consumer class) and `min_window_ns` is one scalar. Either the backend reports a conservative 1 s until a device is validated against integrated power, or the trait grows a validation state. I've taken neither step here. ## The sharper bug: the liveness probe could fail a healthy counter `PROBE_TICK` was 100 ms, justified as *"two of its ~50 ms sample periods"*. Against the real ~100 ms period the tick **equalled** the update period, so roughly half the intervals catch no update, the probe misses `LIVE_SHARE` (four in five) and grades a perfectly healthy counter **not-live** — silently demoting a genuine `HwShunt` reading to the power integral. Now 150 ms, which matches `sandbox-meter`'s `NVML_PROBE_TICK` (whose comment already carried the right derivation), and `MIN_LIVE_ADVANCES` (5) intervals then take 750 ms, inside the 1000 ms `LIVENESS_BUDGET`. **Nothing pinned either constant before.** `the_probe_tick_clears_the_update_period_and_fits_the_budget` now pins both halves. ### Falsification | mutation | result | |---|---| | **F1** `min_window_ns` back to `50_000_000` | **only** `nvml_resolution_is_millijoule_floor` FAILED | | **F2** `PROBE_TICK` back to 100 ms (equal to the period) | **only** the tick test FAILED — the "clears the period" half | | **F3** `PROBE_TICK` to 250 ms | **only** the tick test FAILED — the "fits the budget" half | F2 and F3 hit opposite halves of the same test, so neither half is decoration. ## Not changed **The RAPL floor.** The same paper finds RAPL stable at 10 ms or coarser — which is what §1.3 already says. One caveat worth recording: that is validated *stability*, not validated *accuracy* against an external meter. ## Verification The `nvml` module is **not in a default build**, so a plain `cargo test` proves nothing here: - `cargo test -p jouleclaw-energy --features rapl,nvml` — **84 passed** (CI's `feature_tests` for this row) - `cargo check --workspace --all-targets` + `cargo test --workspace` (`jouleclaw-rs`) — exit **0**, **0** failed, **484** ok suites - `cargo test -p mesh-meter --features hw-meter-nvml` — **14 passed** That last one matters: `jouleclaw-energy` has five consumers outside `jouleclaw/`, and three (`jouledesk-github`, `l3-jouleclaw`, `quantum-meter`) pin `default-features = false, features = ["ioreport"]`, so they never compile this module. Only `mesh-meter`'s `hw-meter-nvml` reaches it — and **CI's mesh row does not build that feature**, so it would not have been caught. No test or doc anywhere pins the old 50 ms figure. `tests/nvml_hardware.rs` ran 0 tests on the build host: it needs a real GPU and only exercises on the `linux-hw` runner. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jouleclaw: the NVML sampling floor is ~100 ms, measured, not ~50 ms
All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 39s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m22s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 58s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 21s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 11s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m19s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 8m28s
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 18m21s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m10s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m4s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 9m20s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 3m22s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 4m32s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 3m6s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 2m13s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 4m42s
f090e6df0f
SPEC §1.3 published a normative floor for the NVML cumulative-energy
counter — "~1 mJ, ~50 ms. `HwShunt`." — feeding the MUST three lines
above it: "Implementations MUST surface `resolution_uj()` and
`min_window_ns()` so callers can refuse to claim microjoule accuracy on
platforms that cannot deliver it." `min_window_ns()` returned
50_000_000, and the figure was restated in three more places.

Dauner et al., *Evaluating the Influence of Measurement Frequency on
Energy Readings Using Intel RAPL and NVIDIA NVML*, ACM SIGENERGY Energy
Informatics Review 6(2), July 2026, measured it across 588 controlled
runs: the cumulative counter **does not update faster than roughly
100 ms on any device they tested**. The stale-read fraction is 93.4 % at
0.5 ms, 86.3 % at 10 ms, 1.0 % at 100 ms, and zero from 500 ms.

So 50 ms does not merely understate the floor — it sits *inside* the
stale-read region, where a read mostly returns the preceding value. We
graded that `HwShunt`, our strongest provenance. A counter read below
its own update rate is not a reading of the span; that is the rule this
family exists to enforce, and our own SPEC broke it.

Two numbers, kept apart on purpose:

- `min_window_ns()` is documented as "the smallest sampling window below
  which readings are noise-dominated" — the counter's UPDATE period.
  That is what the paper measures at ~100 ms, and what this raises it
  to. It is load-bearing, not advisory: `jouledesk-github`'s
  `window_energy` refuses a figure for work shorter than it.
- Accuracy against integrated power above that floor is a DIFFERENT
  property and device-class dependent — an A100 agreed to ±0.2 % at
  every interval, an unvalidated RTX 4090 was still 13.3 % low at 100 ms
  and within 1.6 % only at 1 s. Folding that into the same scalar would
  repeat the category error being fixed, so it is stated in SPEC §1.3
  prose, with the paper's own recommendation: on an unvalidated counter
  prefer the power integral — which `Mode::PowerIntegral` already is,
  graded `ModelBased`.

`PROBE_TICK` also moves, 100 ms → 150 ms, and this is the sharper bug.
Its rationale read "two of its ~50 ms sample periods"; against a real
~100 ms period the tick EQUALLED the update period, so roughly half the
intervals catch no update, the probe misses `LIVE_SHARE` (four in five)
and grades a perfectly healthy counter not-live — demoting a real
`HwShunt` reading to the power integral. 150 ms matches
`sandbox-meter`'s `NVML_PROBE_TICK`, whose comment already carried the
right derivation, and `MIN_LIVE_ADVANCES` (5) intervals then take 750 ms,
inside the 1000 ms `LIVENESS_BUDGET`.

`the_probe_tick_clears_the_update_period_and_fits_the_budget` pins both
halves; nothing pinned either before.

Not changed: the RAPL floor. The same paper finds RAPL stable at 10 ms
or coarser, which is what SPEC §1.3 already says.

Verified with the feature on — `cargo test -p jouleclaw-energy --features
rapl,nvml`, 84 passed — because the `nvml` module is not in a default
build. `tests/nvml_hardware.rs` ran 0 tests here: it needs a real GPU and
only exercises on the `linux-hw` runner.

Co-Authored-By: Claude Opus 5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!316
No description provided.