meter model subprocesses where they ran: the L3 model enters the run's figure #211

Merged
dcharlot merged 12 commits from feat/meter-model-subprocesses into main 2026-10-10 22:25:18 -04:00

2026-10-10

Transaction Science
ci: run the L3 and fabric-meter oracle tests on the macOS energy job
All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 37s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m6s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m48s
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
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m1s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m9s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m35s
standards workspaces build + test / aac-rs (pull_request) Successful in 3m23s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m11s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m11s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 56s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m22s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 39s
standards workspaces build + test / map-rs (pull_request) Successful in 28s
standards workspaces build + test / mesh-rs (pull_request) Successful in 28s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m9s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m23s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
standards workspaces build + test / openpay (pull_request) Successful in 1m19s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m5s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m58s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 9m53s
No job ran the oracle assertions this branch added. joule-code-rs's matrix
entry runs on linux-arm64, where joule-fix's L3 energy test has no tree
meter and takes its fallback branch; jouleclaw-loader-gguf's calibration
tests need the fabric-meter feature, which no job passed, and their oracle
needs macOS.

The macOS energy job now runs both, each step in the job's style (pipefail,
a tee'd log, a grep gate on a line the test prints only where its gated
assertion ran):
- joule-code-remediate `l3_`: 'l3 model process:' (the model's reading
  checked against the stand-in's own count). The stand-in itself stays
  #[ignore]: it is the subprocess the oracle test runs (filtered to it,
  --ignored), not a test of its own, and the oracle test is not ignored.
- jouleclaw-loader-gguf --features "rayon fabric-meter": the marginal_energy
  tests ('calibration row stamped on macOS:') and tests/process_calibration
  ('process calibration: CPU member'); this also builds the fabric-meter
  examples. joule-code-rs's target dir and Cargo.lock join the job's cache.

The l3-jouleclaw example workspace stays out of this job (a heavy separate
build on the one macOS runner); its README says its oracle tests run locally.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:17 -04:00
Transaction Science
l3-jouleclaw: grade a fallback span as no reading; never read the llama engine whole
The docs said a host with no per-process counter gets the token-count
estimate off macOS, but there ProcessSpanMeter falls back to detect_meter()
(the 28.6 W x wall estimator in this build), which it marks Whole, and grade()
kept that figure. A fallback span is the machine's figure, not the engine's,
so grade() now treats it as no reading and uses the rest (host_counter, else
the token estimate): one way of reading the host whichever engine ran, and
the docs hold. That is simpler than documenting a second host figure that
only the in-process engine would get.

JOULE_CODE_L3_SELF_CONTAINED could never take effect: the llama engine is
read at its exit, which the tree meter always marks (TreeGap::Exited), so no
reading was ever whole. The setting and its plumbing are gone and the docs
say the reading is a lower bound, always graded as an estimate.

A missing llama binary had become LlmError::Upstream("... exited 127"): the
binary is exec'd by the gating sh, whose failed exec reads as the model's own
failure. The binary is now resolved first (a path as given, else PATH; a
regular executable file) and one that cannot run is Unavailable, as a failed
spawn was, with nothing spawned.

The README says the oracle tests run only locally on Apple Silicon (CI tests
this workspace on Linux, where they take the fallback), and lines this branch
added name the capability, not a third-party engine.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
jouledesk-github: a model process is never read whole; a fallback carries no model reading
JOULEDESK_LLAMA_SELF_CONTAINED could never take effect. A reading was sealed
HwShunt only with no gap and that declaration, but run_model reads the model
at its exit, and the tree meter marks every member that exited in the span
(TreeGap::Exited: an exiting process's record lacks its last time on a
core). The model always exits, so no reading was ever whole. The setting,
the Llama struct that carried it and the whole-reading branch are gone; the
docs say plainly that the reading is a lower bound and the work is always
sealed as an estimate (the reading plus the host meter's figure). A reading
whose tree showed no gap says why it is still partial (NOT_READ_AT_EXIT).

When the model binary is missing, sh's exec fails (127), the answer falls
back to the deterministic extract, yet the work still carried the reading of
that sh, so the reply named deterministic-extract beside a "model process
... read on its own process tree" line. A fallback now carries no model
reading, as the field's docs said; a test runs a missing binary.

The lines this branch added that named a third-party engine now name the
capability (a model binary that offloads to the GPU, the binary's log).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
joule-code-remediate: seal the L3 window's device classes as separate records
The L3 window's machine-wide figure was Measurement::total_uj(), every
device class summed, and the tally sealed the run's whole figure as one
DeviceClass::Cpu record. Where the machine meter reads a GPU (an Apple GPU
rail, NVML on Linux), that GPU energy was sealed as CPU energy, which the
sandbox schema forbids (a host must not meter accelerator work as
undifferentiated Cpu work).

L3Energy now carries the machine figure per class (machine_by_class;
machine_uj is their sum), the tally keeps the non-CPU part of its estimate
per class, and meter_records() seals one Estimator record per class: the
CPU (cargo windows, the model's own reading, the machine's CPU) and each
other class the meter read. The records sum to the run's figure, so the
receipts' figure is unchanged. An attributed run is still one ModelBased
CPU record: every window attributed means no L3 window.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
joule-code-remediate: drop JOULE_FIX_L3_SELF_CONTAINED, which could never take effect
The L3 window was attributed only for a tree reading with no gap and the
operator's JOULE_FIX_L3_SELF_CONTAINED=1. But run_l3 reads the model at its
exit (waited for, not reaped), and the tree meter marks every member that
exited in the span (TreeGap::Exited): an exiting process's record lacks its
last time on a core. The root always exits, so no reading was ever whole and
the setting, the attributed field and the whole-reading branch were dead —
while the docs offered the setting as the way to a whole reading.

The setting and its plumbing are gone, and the docs now say plainly that a
model subprocess is never read whole: its reading is a lower bound on its CPU
energy, and the window is always an estimate, the reading plus the machine-
wide figure. A reading whose tree showed no gap (only possible if the wait
for the model's exit failed) says so (NOT_READ_AT_EXIT) rather than coming
back with nothing to explain why it is partial.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
jouleclaw-loader-gguf: fabric_calibrate replaces every row of the members it measures
fabric_calibrate merged its rows by keeping every existing row of a member
it did not write this run. A member refused this run (the GPU shared with
another process) or skipped at warm-up kept the row an earlier run took,
so the file came out mixing an old SoC-marginal row (no declared grade,
loaded Benchmarked) with this run's per-process Measured rows, and the
placer, which ranks on uj_per_gflop alone, compared the two as if they
were one kind of figure, while the run said the fabric now placed on
measured figures. A member skipped at warm-up also counted as neither
written nor refused, so the run exited 0.

marginal_energy::merge_calibration now drops every row of the members the
run owns (FABRIC_CALIBRATE_MEMBERS: cpu-neon, amx, metal-gpu) before adding
the rows it measured, so a refused or skipped member falls back to its
seeded figure; rows it does not own (local/metal-mpp, which mpp_energy
writes) stay as they were. A member skipped at warm-up is now refused
(exit 2) and loses any row an earlier shape gave it. mpp_energy already
writes its one row only when measured and nothing otherwise.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
jouleclaw-loader-gguf: pin the process-figure refusal of a partial span of both counters
A span can read both of this process's counters and still be partial (work
declared to use the Neural Engine). process_figure refuses it by its
coverage, which no test drove: the CPU-only case is refused by its source
as well, so dropping the coverage check passed every test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
jouleclaw-loader-gguf: calibrate the fabric on this process's own counters
fabric_calibrate and mpp_energy priced members only on the IOReport SoC
counter, and refused - writing nothing - wherever its CPU rail is frozen,
which is every macOS 27.0.1 host checked: the placer's calibration file could
not be produced at all.

Where IOReport declines they now do what tune_shape_live_process does: each
member's loop is one ProcessSpanMeter span, this process's CPU energy plus
its GPU energy, no idle to subtract. marginal_energy::process_figure is the
rule, pure and in one place: a figure only from a span metered whole on this
process's own counters (not the host-meter fallback), a CPU member's GPU
share an exact zero, a GPU member's GPU time spent (its energy the rail while
this process was the GPU's sole user); anything else is refused, saying why.
Rows are written at the span's grade through calibration_row, with a method
naming the new domain (process CPU + GPU, not the SoC marginal) and, for AMX,
that its Accelerate work is counted as the kernel attributes it to the
calling threads, unchecked against a rail. fabric_calibrate refuses member by
member and writes the rest (exit 2 when any was refused, 1 when none was
written); a member whose last shape was refused gets no row. Both examples
now merge into the file instead of clobbering it. The IOReport path is
unchanged where its rail is live.

Tested: the rule's every refusal, pure; and a live CPU member against an
oracle sharing no code (this process's ri_energy_nj read at the first op's
start and the last op's end, inside the span): equal, twice. On this host the
examples wrote the NEON and AMX rows (Measured) and refused the GPU members:
the GPU was shared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
l3-jouleclaw: test the in-process meter through the path main runs
The in-process engine's span was opened inline in infer(), and the energy
test opened its own: the test exercised the meter but not the call main
makes. Both now go through in_process(), so the oracle test covers what the
binary does (SpanUses::CpuGpu included).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
l3-jouleclaw: meter each engine on its own process, graded by coverage
The run binary bracketed the inference with host_counter() reads: on macOS 27
the load-average estimator, so an in-process Metal decode and a llama.cpp
subprocess were both estimated from host load, although this host reads both
engines' own processes.

The in-process engine (jouleclaw-runtime, Metal kernels) is now metered by
ProcessSpanMeter(gpu_liveness_load) with SpanUses::CpuGpu: this process's CPU
plus its GPU (an exact zero with no GPU time, the rail when it owned the GPU).
The llama engine's subprocess runs behind a gate (sh waits, then execs
"$0" "$@") and is read on its own process tree by TreeMeter, waited for with
WNOWAIT before the reap (LlamaCompletionBackend::complete_metered). grade() is
the one place the receipt's energy is decided, mesh ProcessMeter's rule: a
reading that covers the whole inference keeps its grade; a partial one (GPU
shared, or a child that exited in its window, or undeclared) is an Estimator,
the reading plus the host counter's figure or the token estimate; no reading
leaves that rest as it was. The receipt has no coverage field, so a partial
reading is never sealed HwShunt.

Tested against oracles sharing no code: this process's own ri_energy_nj read
around a CPU burn (span 0.985 of it, GPU an exact zero, Whole, HwShunt), and
a stand-in llama (this test binary) reporting its own count from inside
(reading 0.998 of it, Estimator). The example workspace builds standalone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
jouledesk-github: read the model subprocess on its own process tree
Each mention runs one llama-completion subprocess, metered only by the host
meter: machine-wide, and on macOS 27 the load-average estimator. The model's
own process was never read, although this host counts each process's CPU
energy.

The model now runs behind a gate (sh waits for a line, then execs "$0" "$@",
so arguments are argv, never re-parsed): a TreeMeter rooted at that process
reads it at rest, the gate lets it go, its stdout is drained, it is waited
for with WNOWAIT, the span is stopped on its exited record, then it is
reaped. grade_work is the one place the figure is decided: HwShunt only for a
whole reading of a model the operator declared self-contained
(JOULEDESK_LLAMA_SELF_CONTAINED=1); a partial reading - every model that
exits in its window, and llama.cpp offloading to Metal - is sealed as an
Estimator, the reading plus the host meter's figure, because efficiency/1 has
no coverage field and its figure stands for the whole; a partial reading with
no host figure is no figure and is not sealed; no reading leaves the host
meter's figure as it was. The reply carries the reading and why it is partial
on its own line, on GitHub and Slack.

Tested against an oracle sharing no code: a stand-in model (this test binary
behind a script taking the model's arguments) reports its own ri_energy_nj
from inside; the reading was 0.998-0.999 of it, declared and undeclared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00
Transaction Science
joule-code-remediate: meter the L3 model's window into the run's figure
l3_pass ran the L3 command under no meter at all, so RunReport::total_uj,
uj_per_verified_fix and both receipts omitted the model's energy: the most
expensive tier of a run was the one the figure left out.

The command now runs under sh, which waits for a gate line and then execs a
plain argv, so the model is the very process a TreeMeter is rooted at and the
shell's start-up stays outside the window. The tree meter reads it at rest,
the machine-wide span opens, the gate and prompt are written, stdout is
drained, the child is waited for with WNOWAIT and the tree span stopped on its
exited record, then it is reaped. grade_l3 is the one place the window is
graded: attributed only for a whole reading of a command the operator
declared self-contained (JOULE_FIX_L3_SELF_CONTAINED, the gateway's rule);
any partial reading - every model that exits in its window has the Exited gap
- is an estimate, the reading plus the machine-wide figure (mesh's
ProcessMeter rule), never a hardware figure of the whole; no reading (Linux,
counter not live) is the machine-wide figure as a cargo window's. JCR-1
DeviceEnergy and JouleContract EnergyClass carry no coverage, so a partial
window grades the run down, never up. RunReport::l3 says what was read and
why it is partial.

Tested against an oracle sharing no code: a stand-in model (this test binary)
reports its own ri_energy_nj from inside; the window read 0.999 of it, twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 22:24:01 -04:00