jouleclaw: bound a GPU energy counter from below, not only above #341

Merged
dcharlot merged 1 commit from fix/gpu-counter-under-reports into main 2026-10-19 21:21:43 -04:00
Owner

GpuPlausibility judged an NVML cumulative-energy counter against its board's own telemetry in one direction only. Its three verdicts were Plausible, CounterOverRate ("ran faster than the board can draw") and BoardTelemetryUnusable. Nothing caught a counter reading low.

That is the wrong direction to guard, by this family's own doctrine — jcp/SPEC.md §5:

Under-reporting is the unsafe direction at a ceiling, because it admits work the budget cannot cover.

A counter running fast fails safe: it spends a budget sooner than it should. A counter reading low is graded HwShunt and then spends an energy ceiling it is not measuring. And jouleclaw/SPEC.md §1.3 already refused a counter "republished in lumps" — but nothing implemented it, because such a counter advances, so the sustained-advance liveness probe calls it live while it accrues a fraction of the energy. The spec said it; the code did not do it.

The bound

CounterUnderRate, derived from the same board telemetry the ceiling bound already uses — so no new threshold is invented from nowhere:

floor = (mean_usable_draw − GPU_DRAW_SLACK_MW) / GPU_UNDER_FACTOR
  • The slack comes off the draw before the division, so the floor collapses to zero at or below 5 W of mean draw. On an idle board a correct counter accrues almost nothing, and refuting it would be noise rather than a finding.
  • GPU_UNDER_FACTOR (4) is deliberately looser than GPU_DRAW_FACTOR (2): the draw readings are one-second averages, so a bursty load that is idle between samples really does integrate to less than its sampled mean.
  • The verdict carries the yardstick it was judged against (mean_draw_mw), so the judgement can be re-made against a different factor later — the same reason the existing verdicts carry their figures.

It ranks above a proven overrun. Both refute, but only this one admits work a budget cannot cover, so where one GPU of a probe over-reports and another under-reports, the under-report is the verdict an operator must act on. Among two under-reporting GPUs the ordering is by shortfall, not by rate.

mode_for needed no change: Implausible already falls back to the vendor power integral and grades ModelBased, so the fix completes itself once the verdict exists and maps. (The reported finding also claimed mode_for performed no fallback — that half is false on current main; it already handles Frozen | Unavailable | Implausible | Unjudgeable.)

Two things found beyond the reported gap

  • _ => CounterLiveness::Live in the verdict→liveness mapping meant a newly added verdict would silently become live, and "unrecognised verdict is admissible" is the unsafe default. That match is now exhaustive: a future arm must be graded deliberately or the crate does not compile.
  • The wiring was untested. The existing test covered plausible_gpu_counter and the over-rate path through GpuWatch::liveness, but a verdict that never reaches CounterLiveness changes nothing — the counter would still be graded HwShunt. This is the same gap falsification caught in #328, so it now has a test driving an under-reporting GPU through the probe.

Falsification

mutation result
U1 the under-rate bound removed entirely a_gpu_counter_must_count_no_slower_than_its_board_draws + 3 FAILED
U2 slack not taken off the draw before dividing (fires at idle) an_idle_board_yields_no_under_rate_finding + 2 FAILED
U3 the floor is inclusive at its boundary only a_gpu_counter_must_count_no_slower_than_its_board_draws FAILED
U4 under-reporting ranked below over-reporting only an_under_reporting_gpu_speaks_for_a_probe_before_an_over_reporting_one FAILED
U5 the verdict never reaches CounterLiveness (wiring dropped) an_under_reporting_gpu_makes_the_probe_implausible + 1 FAILED
U6 floor uses the tight ceiling factor instead of the loose one a_gpu_counter_must_count_no_slower_than_its_board_draws + 1 FAILED

All six killed by their named test, each having been checked for anchor uniqueness, that the edit landed, that it still compiled, and that the named test actually failed — four conditions, because any one missing makes "SURVIVED" ambiguous between "pinned", "my mutation was wrong" and "nothing was tested".

The headline test's discriminating half is the one that matters: the same slow counter beside an idle board must stay live. Nothing about the counter changes — only the yardstick.

U5 also broke the pre-existing every_gpu_must_count_no_faster_than_its_board_draws. Its "draws swapped" case (a GPU counting 10 W against a 290 W board) is now refuted from below as well as its sibling from above; since under-reporting outranks over-reporting, that is the verdict which now speaks. Same Implausible outcome, stronger reason.

Verification

  • cargo check --workspace --all-targets --locked (jouleclaw-rs, RUSTFLAGS=-C target-feature=+fp16) — exit 0
  • cargo test --workspace --locked — exit 0, 484 test binaries green, no failures
  • cargo check -p jouleclaw-energy --features rapl,nvml --all-targets --locked — exit 0 (the feature leg CI runs separately)
  • cargo clippy -p jouleclaw-energy --all-targets --no-deps --locked --features ioreport -- -D warnings — clean
  • check-spec-refs 1671 citations / 16 standards, check-comparative-prose and check-no-vanity-metrics clean

Everything touched is pub(crate) or private — zero bare pub items added — so the public surface is unchanged and the four other workspaces holding jouleclaw-energy as a path dep (jouledesk-rs, mesh-rs, wai/tools/quantum-meter, joule-code/examples/l3-jouleclaw) cannot be affected by construction; the gate builds them regardless.

Pre-existing and not touched here: cargo doc reports three unresolved intra-doc links elsewhere in this crate (process_cpu_liveness, FOLD_SLEEPS, this_thread_energy_nj). None are in the code this PR changes, and cargo doc is not part of this workspace's CI gate.

🤖 Generated with Claude Code

`GpuPlausibility` judged an NVML cumulative-energy counter against its board's own telemetry **in one direction only**. Its three verdicts were `Plausible`, `CounterOverRate` ("ran faster than the board can draw") and `BoardTelemetryUnusable`. Nothing caught a counter reading **low**. That is the wrong direction to guard, by this family's own doctrine — `jcp/SPEC.md` §5: > Under-reporting is the unsafe direction at a ceiling, because it admits work the budget cannot cover. A counter running fast **fails safe**: it spends a budget sooner than it should. A counter reading low is graded `HwShunt` and then spends an energy ceiling it is **not measuring**. And `jouleclaw/SPEC.md` §1.3 already refused a counter "republished in lumps" — but nothing implemented it, because such a counter *advances*, so the sustained-advance liveness probe calls it live while it accrues a fraction of the energy. The spec said it; the code did not do it. ## The bound `CounterUnderRate`, derived from the **same** board telemetry the ceiling bound already uses — so no new threshold is invented from nowhere: ``` floor = (mean_usable_draw − GPU_DRAW_SLACK_MW) / GPU_UNDER_FACTOR ``` - The slack comes off the draw **before** the division, so the floor collapses to **zero** at or below 5 W of mean draw. On an idle board a correct counter accrues almost nothing, and refuting it would be noise rather than a finding. - `GPU_UNDER_FACTOR` (4) is deliberately **looser** than `GPU_DRAW_FACTOR` (2): the draw readings are one-second averages, so a bursty load that is idle between samples really does integrate to less than its sampled mean. - The verdict carries the **yardstick** it was judged against (`mean_draw_mw`), so the judgement can be re-made against a different factor later — the same reason the existing verdicts carry their figures. It ranks **above** a proven overrun. Both refute, but only this one admits work a budget cannot cover, so where one GPU of a probe over-reports and another under-reports, the under-report is the verdict an operator must act on. Among two under-reporting GPUs the ordering is by **shortfall**, not by rate. `mode_for` needed no change: `Implausible` already falls back to the vendor power integral and grades `ModelBased`, so the fix completes itself once the verdict exists and maps. (The reported finding also claimed `mode_for` performed no fallback — that half is **false on current main**; it already handles `Frozen | Unavailable | Implausible | Unjudgeable`.) ## Two things found beyond the reported gap - **`_ => CounterLiveness::Live`** in the verdict→liveness mapping meant a newly added verdict would **silently become live**, and "unrecognised verdict is admissible" is the unsafe default. That match is now exhaustive: a future arm must be graded deliberately or the crate does not compile. - **The wiring was untested.** The existing test covered `plausible_gpu_counter` and the over-rate path through `GpuWatch::liveness`, but a verdict that never reaches `CounterLiveness` changes nothing — the counter would still be graded `HwShunt`. This is the same gap falsification caught in #328, so it now has a test driving an under-reporting GPU through the probe. ## Falsification | mutation | result | |---|---| | **U1** the under-rate bound removed entirely | `a_gpu_counter_must_count_no_slower_than_its_board_draws` + 3 FAILED | | **U2** slack not taken off the draw before dividing (fires at idle) | `an_idle_board_yields_no_under_rate_finding` + 2 FAILED | | **U3** the floor is inclusive at its boundary | **only** `a_gpu_counter_must_count_no_slower_than_its_board_draws` FAILED | | **U4** under-reporting ranked below over-reporting | **only** `an_under_reporting_gpu_speaks_for_a_probe_before_an_over_reporting_one` FAILED | | **U5** the verdict never reaches `CounterLiveness` (wiring dropped) | `an_under_reporting_gpu_makes_the_probe_implausible` + 1 FAILED | | **U6** floor uses the tight ceiling factor instead of the loose one | `a_gpu_counter_must_count_no_slower_than_its_board_draws` + 1 FAILED | All six killed by their named test, each having been checked for anchor uniqueness, that the edit landed, that it still **compiled**, and that the named test actually failed — four conditions, because any one missing makes "SURVIVED" ambiguous between "pinned", "my mutation was wrong" and "nothing was tested". The headline test's discriminating half is the one that matters: the **same slow counter beside an idle board** must stay live. Nothing about the counter changes — only the yardstick. U5 also broke the pre-existing `every_gpu_must_count_no_faster_than_its_board_draws`. Its "draws swapped" case (a GPU counting 10 W against a 290 W board) is now refuted from **below** as well as its sibling from above; since under-reporting outranks over-reporting, that is the verdict which now speaks. Same `Implausible` outcome, stronger reason. ## Verification - `cargo check --workspace --all-targets --locked` (`jouleclaw-rs`, `RUSTFLAGS=-C target-feature=+fp16`) — exit **0** - `cargo test --workspace --locked` — exit **0**, **484** test binaries green, no failures - `cargo check -p jouleclaw-energy --features rapl,nvml --all-targets --locked` — exit **0** (the feature leg CI runs separately) - `cargo clippy -p jouleclaw-energy --all-targets --no-deps --locked --features ioreport -- -D warnings` — clean - `check-spec-refs` 1671 citations / 16 standards, `check-comparative-prose` and `check-no-vanity-metrics` clean Everything touched is `pub(crate)` or private — **zero** bare `pub` items added — so the public surface is unchanged and the four other workspaces holding `jouleclaw-energy` as a path dep (`jouledesk-rs`, `mesh-rs`, `wai/tools/quantum-meter`, `joule-code/examples/l3-jouleclaw`) cannot be affected by construction; the gate builds them regardless. Pre-existing and **not** touched here: `cargo doc` reports three unresolved intra-doc links elsewhere in this crate (`process_cpu_liveness`, `FOLD_SLEEPS`, `this_thread_energy_nj`). None are in the code this PR changes, and `cargo doc` is not part of this workspace's CI gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jouleclaw: bound a GPU energy counter from below, not only above
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 8s
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 41s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 52s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m11s
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 1m31s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m15s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m20s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 7m22s
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 8m24s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m15s
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 18m16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m56s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 6m34s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 5m7s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 3m10s
04086d2e38
`GpuPlausibility` judged an NVML cumulative-energy counter against the board's
own telemetry in one direction only. Its three verdicts were `Plausible`,
`CounterOverRate` ("ran faster than the board can draw") and
`BoardTelemetryUnusable`. Nothing caught a counter reading **low**.

That is the wrong direction to guard. By this family's own doctrine (jcp/SPEC
§5): "Under-reporting is the unsafe direction at a ceiling, because it admits
work the budget cannot cover." A counter running fast fails safe — it spends a
budget sooner. A counter reading low is graded `HwShunt` and then spends an
energy ceiling it is not measuring. jouleclaw/SPEC §1.3 already refused a
counter "republished in lumps", but nothing implemented it: such a counter
*advances*, so the sustained-advance liveness probe calls it live while it
accrues a fraction of the energy.

`CounterUnderRate` closes it, bounded by the **same** board telemetry the
ceiling bound already uses, so no threshold is invented:

    floor = (mean_usable_draw - GPU_DRAW_SLACK_MW) / GPU_UNDER_FACTOR

The slack comes off the draw *before* the division, so the floor collapses to
zero at or below 5 W of mean draw: on an idle board a correct counter accrues
almost nothing, and refuting it would be noise rather than a finding. The
factor is deliberately looser than `GPU_DRAW_FACTOR` because the draw readings
are one-second averages, so a bursty load idle between samples really does
integrate to less than its sampled mean. The verdict carries the yardstick it
was judged against, so the judgement can be re-made against a different factor.

It ranks **above** a proven overrun: both refute, but only this one admits work
a budget cannot cover, so where one GPU of a probe over-reports and another
under-reports, the under-report is what an operator must act on. Among two
under-reporting GPUs the ordering is by shortfall, not by rate.

Two things found while doing it, beyond the reported gap:

- **`_ => CounterLiveness::Live` in the verdict mapping** meant a newly added
  verdict would silently become *live* — and "unrecognised verdict is
  admissible" is the unsafe default. That match is now exhaustive, so a future
  arm must be graded deliberately or the crate does not compile.
- **The wiring was untested.** The existing test covered
  `plausible_gpu_counter` and the over-rate path through `GpuWatch::liveness`,
  but a verdict that never reaches `CounterLiveness` changes nothing. Added a
  test driving an under-reporting GPU through the probe, whose discriminating
  half is the *same slow counter beside an idle board* — which must stay live,
  because nothing about the counter changed, only the yardstick.

`mode_for` needed no change: `Implausible` already falls back to the vendor
power integral and grades `ModelBased`, so the fix completes itself once the
verdict exists and maps. SPEC §1.3 now states the bound is two-sided, that it
refutes and never verifies, that an indeterminate is not hardware-graded, and
that it must not fire near idle. Doc comments that described the bound as
one-directional are corrected.

Figures throughout are the jetson-hub RTX 4050's own, as the existing test's
already are.

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!341
No description provided.