jouleclaw-energy: per-process GPU energy on Apple Silicon; a shared GPU is never split by time #146
Loading…
Reference in a new issue
No description provided.
Delete branch "jouleclaw-energy/apple-gpu-share"
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?
Per-process GPU energy on Apple Silicon, without root, alongside the per-process CPU counter, so a Metal inference span is metered on both.
What the hardware offers (M5 Max, macOS 27.0.1, unprivileged; measured)
Energy Model/GPU Energy(nJ) is live; every mJ channel (CPU Energy,GPU0,ANE0,DRAM0, …) is frozen for an unprivileged reader. The rail republishes in steps, sometimes holding 300–600 ms under load, so a snapshot reads it on its next update.IOUserClientCreator(its pid) andAppUsage.accumulatedGPUTime(ns). This is read through IOKit, unprivileged.task_power_info_v2.gpu_energy.task_gpu_utilisationis always 0.task_energyequals the CPU counterri_energy_nj.ANE0is frozen, and ANE work runs inaned, whose clients carry no statistics.The rule
This process used no GPU time: its GPU energy is exactly 0. That is graded HwShunt and needs no rail, so other clients can't disturb it, and CPU-only work keeps its CPU grade.
It had ≥99% of all clients' GPU time: it was effectively the GPU's sole user, and its GPU figure is the rail's reading over the span, graded HwShunt. As with RAPL's package figure, idle and static draw are included.
Otherwise, no GPU figure (
GpuShared). Time share is not energy share:SPEC §1.3 now says the rail MUST NOT be split by GPU time.
A client that vanishes mid-span (its process exited) makes every share unknown, so there is no GPU figure.
A span (
ProcessSpanMeter) sums CPU and GPU at the weaker grade, as Whole, or Partial if the caller declares ANE use. When the GPU has no figure, the span is the CPU figure alone: HwShunt, Partial, with the reason recorded (SpanSource::CpuOnly). The host meter is the fallback only when the CPU counter itself gives none.Liveness: the family rule (≥5 advances, ≥80% of intervals, a window of ≥300 ms) over the GPU rail and this process's own GPU time, while the caller's closure loads the GPU. It is probed once per process. A CPU-only load returns Frozen.
Wiring
gpu_liveness_load(), a small Metal dispatch for the probe (objc2-metal, as the fabric uses).fabric-meter: the live tune falls back to process CPU+GPU spans when IOReport is declined, as it always is on macOS 27. Before this it returnedNone. Only Whole spans compete; a Partial span cancels the tune.span_joulesbills only a Whole figure.gguf_energyexample prints CPU and GPU joules per token, with grade, coverage and share.Measured
ioreggave the same numbers.GpuShared, so the span is CPU only and Partial. CPU-only work stays Whole with a GPU figure of exactly 0. With a child exiting mid-span, the span is CPU only and Partial, and records why.Q8_PACKED=1):Verified
cargo test -p jouleclaw-energy --features ioreportpasses all 121 tests, including the GPU hardware test, on this Mac.cargo check --workspace --all-targetspasses for jouleclaw-rs. The harness-econ, loader-gguf (fabric-meter) and mesh-meter tests pass.GpuSharedcheck removed; CPU-only replaced by the fallback meter.🤖 Generated with Claude Code
The time-share model is falsified by this branch's own measurements (a GGUF decode at ~1.9 W per GPU-second beside a ~17 W job; contended figures 3-15x the sole-user figure), so it is no longer a figure: - apportion(): this process's own GPU time first, from its own clients (one gone: GpuClientVanished). None spent: 0 uJ, exact, HwShunt, with no rail, currency or other-client check. Spent, but under 99% of every client's time: EnergyError::GpuShared { self_ns, all_ns }. ModelBased is no longer a GPU grade; GpuShare.provenance is now a method (HwShunt), rail_uj / all_ns are None for an exact zero. - combine(): CPU figure and no GPU figure is SpanSource::CpuOnly, the CPU alone, HwShunt, Partial, with why; the fallback meter only when the CPU counter gives none. - fabric live tune: only spans metered Whole compete; a Partial one voids the tune. harness-econ refuses CpuOnly as a dispatch total. - gguf_energy prints the CPU-only line; the hardware test expects GpuShared and CpuOnly under contention and exact zeros beside a burning child and across its exit. Co-Authored-By: Claude Opus 5.5 <[email protected]>