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]>
32 lines
1.4 KiB
Python
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'}")
|