open-standards/jcp/tools/counter-resolution/counter_resolution.py
Transaction Science 421b7e9b71 jcp-energy: survey every accelerator, and refuse a span the counter cannot resolve
Heterogeneous compute is the premise, so the taxonomy had to stop being three
Apple rails. `Fabric` now covers `Cpu`, `Gpu`, `Npu`, `Fpga`, `Dsp`, `Dram` and
`Package` — `Ane` is gone, since Apple's Neural Engine, a Qualcomm or Intel
NPU, a TPU, Ascend, Gaudi and Cambricon are one class of silicon and the vendor
belongs in the source, not the fabric.

`FabricSource` names the counter: `ProcRusage`, `IoReport`, `Powermetrics`,
`Rapl`, `Nvml`, `RocmSmi`, `LevelZero`, `TegraIna`, `QualcommQpt`, `AscendSmi`,
`HabanaSmi`, `CambriconCnmon`, `ExternalMeter`. A receipt can now say *which*
counter produced a figure: two readings graded `HwShunt` from different sources
are not interchangeable, because they have different domains, different update
periods and different attribution.

`survey()` returns every `(fabric, source)` pair with this host's status, and
the **five negative statuses are the substance**:

  - `Frozen` — silicon and counter both present, counter not advancing.
  - `Absent` — no such silicon here.
  - `NeedsPrivilege` — present, needs root we do not have.
  - `NoCounterExists` — present, and exposes no energy counter at all, so any
    figure is modelled by anyone. A property of the hardware.
  - `NotImplemented` — a counter exists and we have no reader. Our gap, named.

Blurring those is what makes an energy survey worthless, and one of them is
load-bearing outside this crate: a disclosure regime that excuses a provider
who "lacks critical information from the compute or hardware provider" is
relying on `NoCounterExists`, and this distinguishes it from the three statuses
that are nobody's hardware's fault. Presence is probed (`/sys/class/kfd`,
`/dev/accel/accel0`, `/dev/davinci0`, `/dev/hl0`, `/dev/cambricon_dev0`,
`/sys/bus/i2c/drivers/ina3221`, …) so an unread accelerator reports
`NotImplemented` rather than the false claim `Absent`. Apple's rails are
probed live — two reads with a CPU burn between them — because which rails a
macOS version exposes to an unprivileged reader is a fact to observe on the
host, not to assume.

**And the refusal is now real.** `FabricStatus::may_grade_span` enforces what
`min_window_ns` never did: a span shorter than the counter's update period is
not a measurement of that span, and a counter whose period is **undeclared**
can never grade a span at any length, because an unknown error direction at a
ceiling must be read as possibly-under-reporting.

That rule immediately indicted our own best figure, which is how it earned its
place: the per-process CPU counter had an undeclared period, so the only
process-attributed reading on Apple silicon could not be graded. Rather than
weaken the rule, `tools/counter-resolution/` measures the counter. Burning this
thread's CPU and sampling 60 times per interval, the fraction of intervals in
which `ri_energy_nj` advanced: **0% at 10 µs and 50 µs, 2% at 100 µs, 5% at
500 µs, 35% at 1 ms, 63% at 2 ms, 87% at 5 ms, 100% at 10 ms**. The crate's
liveness criterion is 80%, so 5 ms is where it first clears — and **10 ms is
declared**, because over-declaring refuses work that would have fit while
under-declaring admits work the budget cannot cover.

Below 1 ms it does not lag, it **batches**: on the few intervals where it moved
at all the median step was ~20 mJ, two orders above the 1 ms step. A sampler
reading it every 500 µs would see nothing most of the time and a 20 mJ jump
occasionally, and would report both as measurements — the same pathology the
crate already refuses on IOReport's CPU rails, and the same shape as the
published 95.4% under-read at 0.5 ms sampling.

With the period declared, this morning's figure stands for the reason it was
taken that way: 2.606 s ≫ 10 ms, which is why it is the mean of 200 decisions
over one long span and not 200 counter reads.

98 tests pass, 0 clippy findings. Six new invariants: each pair named once, a
source with no backend is never `Live`, an undeclared period grades nothing at
any span, source names are stable and distinct, only `ProcRusage` may claim
per-process attribution, and the declared period refuses exactly the intervals
the measurement showed it cannot resolve.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-05 07:20:58 -04:00

32 lines
1.4 KiB
Python

"""At what interval does ri_energy_nj actually advance under continuous load?
The crate's own liveness criterion: a rail is live when it advanced in >=80% of
sampled intervals. Applied here to find the shortest interval that clears it,
which is the counter's declarable update period.
"""
import ctypes, os, time, statistics
_l = ctypes.CDLL("/usr/lib/libSystem.B.dylib"); _b = ctypes.create_string_buffer(464); _p = os.getpid()
def nj():
_l.proc_pid_rusage(ctypes.c_int(_p), ctypes.c_int(6), ctypes.byref(_b))
return int.from_bytes(_b.raw[336:344], "little")
def trial(interval_s, n=60):
"""Burn for `interval_s` per sample; report the fraction that advanced."""
adv = 0; deltas = []
prev = nj()
for _ in range(n):
t_end = time.perf_counter() + interval_s
x = 0
while time.perf_counter() < t_end:
x += 1 # burn this thread's CPU
cur = nj()
if cur > prev: adv += 1
deltas.append(cur - prev)
prev = cur
nz = [d for d in deltas if d > 0]
return adv / n, (statistics.median(nz)/1e3 if nz else 0.0)
print(f"{'interval':>10} {'advanced':>9} {'median ΔµJ':>11} live(>=80%)")
for us in (10, 50, 100, 250, 500, 1_000, 2_000, 5_000, 10_000, 20_000, 50_000):
frac, med = trial(us/1e6)
print(f"{us:>8}µs {frac*100:>8.0f}% {med:>11.1f} {'yes' if frac >= 0.80 else 'no'}")