open-standards/mesh/tools/joules-per-decision/gpu_share.py
Transaction Science 5208dc5936
All checks were successful
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m57s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m29s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m13s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m43s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 8m28s
standards workspaces build + test / aac-rs (pull_request) Successful in 3m42s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 3m18s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 1m20s
standards workspaces build + test / map-rs (pull_request) Successful in 1m56s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m6s
standards workspaces build + test / openpay (pull_request) Successful in 7m12s
standards workspaces build + test / arl-rs (pull_request) Successful in 3m41s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 5m16s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 4m18s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 7m16s
standards workspaces build + test / sense-rs (pull_request) Successful in 4m7s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m30s
mesh: withdraw the GPU per-decision figure — another process held 99.5% of the GPU
I reported 0.104 J per three-question decision as GPU 0.0966 J plus process CPU
0.0075 J, and put it in SPEC §3.6's worked example. The GPU half does not hold.

`gpu_share.py` measures the thing I had assumed away: per-process GPU time from
`accumulatedGPUTime` on each IOUserClient in the IOAccelerator subtree, as a
**delta over the span** rather than the cumulative totals (which are useless —
WindowServer has days of accrued time). Over a 4.06 s span with an MLX matmul
loop running, **com.apple.Safari held 2.470 s of 2.482 s — 99.5%** — while this
process registered 0.000 s. The host was at load average 132 on 18 CPUs,
running another session's VVC/VTM encodes, which is this machine's normal state
rather than an unlucky moment.

A rail delta over that span is a reading of Safari. The rule already shipped
elsewhere in the fleet is the right one: attribute a rail reading to this
process only when it holds ≥99% of the GPU time advancing in the span, report
exactly zero when it holds none, and otherwise **issue no GPU figure**, marking
the span CPU-only and partial. By that rule this measurement issues none.

The 0.08% idle-bracket correction I cited as evidence of cleanliness proves
nothing, and I should not have read it that way: a bracket subtracts *steady*
background, and cannot subtract a contending process whose work overlaps the
span but not the brackets.

What stands, and why it is the more interesting half: the **CPU** figures are
untouched — Laya 0.486 J and GLiNER2.5-Decide 0.555 J per three-question
decision — because the kernel's per-process counter attributes to a principal,
so neither load average 132 nor Safari's GPU work enters them. That is exactly
what `Attribution::Process` is for, and it took a contaminated GPU figure to
demonstrate it. SPEC §3.6's worked example now uses the CPU figure (486 000 µJ,
162 mJ per answered question) and says why it is not the GPU one. Latency
figures are unaffected.

Also corrects the direction of my earlier framing. I presented the GPU path as
"4.7× cheaper, and the price is that it cannot be bound to a principal". The
truth is stronger: on a shared host it cannot be bound to a principal *and*
the number itself is not the workload's. A sound GPU figure needs a host with
no display server and no other GPU client — on this fleet, a headless Linux box
with NVML, not a Mac.

`gpu_rail_period.py` is also added, the harness behind 92449670's correction of
the rail's declared period.

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

49 lines
2 KiB
Python

"""My process's share of GPU time over a span — the #146 admissibility rule.
accumulatedGPUTime is cumulative since client creation, so WindowServer always
dominates the totals. The question that decides whether a rail reading may be
attributed to this process is the DELTA over the measured span.
"""
import re, subprocess, time, os, sys, threading
def gpu_times():
out = subprocess.run(["ioreg","-r","-c","IOAccelerator","-l","-w0"],
capture_output=True, text=True, timeout=30).stdout
tot = {}
for b in out.split("IOUserClientCreator")[1:]:
m = re.search(r'pid (\d+), ([^"]+)', b)
if not m: continue
key = (int(m.group(1)), m.group(2).strip())
g = sum(int(x) for x in re.findall(r'"accumulatedGPUTime"\s*=\s*(\d+)', b[:4000]))
if g: tot[key] = tot.get(key, 0) + g
return tot
def load(stop):
import mlx.core as mx
a = mx.random.normal((2048,2048)); b = mx.random.normal((2048,2048))
while not stop.is_set():
mx.eval(a @ b)
me = os.getpid()
stop = threading.Event()
t = threading.Thread(target=load, args=(stop,), daemon=True); t.start()
time.sleep(0.5)
before = gpu_times(); t0 = time.perf_counter()
time.sleep(4.0)
after = gpu_times(); span = time.perf_counter() - t0
stop.set(); t.join(timeout=3)
delta = {}
for k, v in after.items():
d = v - before.get(k, 0)
if d > 0: delta[k] = d
s = sum(delta.values())
mine = sum(v for (pid, _n), v in delta.items() if pid == me)
print(f"span {span:.2f}s clients advancing: {len(delta)} total GPU time in span: {s/1e9:.3f}s")
print(f"this process (pid {me}): {mine/1e9:.3f}s = {100*mine/s if s else 0:.1f}% of GPU time in the span")
for (pid, n), v in sorted(delta.items(), key=lambda kv: -kv[1])[:6]:
tag = " <-- me" if pid == me else ""
print(f" pid {pid:>7} {n[:34]:<34} {v/1e9:>8.3f}s {100*v/s:>5.1f}%{tag}")
print()
print("#146 rule:", "HwShunt (>=99%)" if s and mine/s >= 0.99 else
("exactly 0 (no GPU time)" if mine == 0 else "NO GPU FIGURE — span is CPU only, Partial"))