jouleclaw: bound a GPU energy counter from below, not only above #341
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/gpu-counter-under-reports"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
GpuPlausibilityjudged an NVML cumulative-energy counter against its board's own telemetry in one direction only. Its three verdicts werePlausible,CounterOverRate("ran faster than the board can draw") andBoardTelemetryUnusable. Nothing caught a counter reading low.That is the wrong direction to guard, by this family's own doctrine —
jcp/SPEC.md§5:A counter running fast fails safe: it spends a budget sooner than it should. A counter reading low is graded
HwShuntand then spends an energy ceiling it is not measuring. Andjouleclaw/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:GPU_UNDER_FACTOR(4) is deliberately looser thanGPU_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.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_forneeded no change:Implausiblealready falls back to the vendor power integral and gradesModelBased, so the fix completes itself once the verdict exists and maps. (The reported finding also claimedmode_forperformed no fallback — that half is false on current main; it already handlesFrozen | Unavailable | Implausible | Unjudgeable.)Two things found beyond the reported gap
_ => CounterLiveness::Livein 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.plausible_gpu_counterand the over-rate path throughGpuWatch::liveness, but a verdict that never reachesCounterLivenesschanges nothing — the counter would still be gradedHwShunt. This is the same gap falsification caught in #328, so it now has a test driving an under-reporting GPU through the probe.Falsification
a_gpu_counter_must_count_no_slower_than_its_board_draws+ 3 FAILEDan_idle_board_yields_no_under_rate_finding+ 2 FAILEDa_gpu_counter_must_count_no_slower_than_its_board_drawsFAILEDan_under_reporting_gpu_speaks_for_a_probe_before_an_over_reporting_oneFAILEDCounterLiveness(wiring dropped)an_under_reporting_gpu_makes_the_probe_implausible+ 1 FAILEDa_gpu_counter_must_count_no_slower_than_its_board_draws+ 1 FAILEDAll 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. SameImplausibleoutcome, stronger reason.Verification
cargo check --workspace --all-targets --locked(jouleclaw-rs,RUSTFLAGS=-C target-feature=+fp16) — exit 0cargo test --workspace --locked— exit 0, 484 test binaries green, no failurescargo 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— cleancheck-spec-refs1671 citations / 16 standards,check-comparative-proseandcheck-no-vanity-metricscleanEverything touched is
pub(crate)or private — zero barepubitems added — so the public surface is unchanged and the four other workspaces holdingjouleclaw-energyas 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 docreports 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, andcargo docis not part of this workspace's CI gate.🤖 Generated with Claude Code
`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]>