jouleclaw: bound a GPU energy counter from below, not only above #341
Merged
dcharlot
merged 1 commit from 2026-10-19 21:21:43 -04:00
fix/gpu-counter-under-reports into main 2026-10-19
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
`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]>