All checks were successful
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (pull_request) Successful in 43s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m30s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 12s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m46s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 4m2s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m55s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 6m0s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m36s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m55s
Builds jouleclaw-energy with `smc` in the two consumers that bill work with the host meter. On macOS 27, where IOReport's CPU rails are frozen for an unprivileged reader, their host meter becomes the SMC's whole-machine power integral (ModelBased, domain System) instead of the load-average estimator; a live IOReport counter still comes first. - jouledesk-github: `detect_meter` picks the SMC; replies say "modeled by `apple-smc`" and seal ModelBased. Work shorter than the SMC's 2 s minimum window is posted as not metered, with why, as it already was for any meter. The model-subprocess test calibrated its stand-in to about a second of CPU, under that window; it now runs a second past the host meter's minimum window at the loop's measured rate, so it holds for every meter. - Joule Code L3 example: `host_counter` adds the SMC between IOReport and the estimator. A counter's reading after a window shorter than its minimum is no longer taken (`read_after`): the receipt falls back to the token estimate, as it does with no counter. The host test burns past the counter's minimum and asserts the grade each meter gives; a new test covers the window rule. Not changed: crates that enable `ioreport` only for this process's own counters and refuse the span meter's host fallback (jouleclaw-loader-gguf, wai quantum-meter and ops-gpu-bench) or only print it (jouleclaw-runtime's example), where the SMC would start a sampler nothing reads; mesh-meter, whose HwMeter does not consult a counter's minimum window, so short served requests would carry ModelBased figures below the SMC's 2 s floor. On an M3 Ultra Mac Studio (IOReport CPU rails frozen): jouledesk-github seals work at ModelBased by `apple-smc` and the model-subprocess rounds read the model's own CPU (21 J against its own 21 J count) plus the SMC's figure, posted as an estimate; the L3 host test meters its burn at ModelBased. Tests pass on macOS (jouledesk-github, l3-jouleclaw) and Linux (l3-jouleclaw, token estimate); clippy is clean for jouledesk-github. l3-jouleclaw's clippy reports one manual_char_comparison in src/llama_backend.rs, which this change does not touch and CI does not lint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
5.2 KiB
Markdown
97 lines
5.2 KiB
Markdown
# L3 binding — JouleClaw model tier → Joule Code cascade
|
|
|
|
A consumer **binding example**: how to wire JouleClaw's **L3 (Model)** tier into
|
|
the Joule Code cascade. It is a standalone workspace, so the standard's reference
|
|
library keeps no JouleClaw dependency.
|
|
|
|
A coding request that misses the cheaper tiers (L0 cache, L1 lawful, L2 embed) is
|
|
generated by a local model at L3, producing a content-addressed `CodingOp` and a
|
|
joules-class `Receipt`.
|
|
|
|
## Three backends, same slot
|
|
|
|
All implement JouleClaw's `jouleclaw_llm_cheap::LlmBackend`, so they drop into
|
|
the same cascade stage:
|
|
|
|
- **`EchoBackend`** (from JouleClaw) — deterministic, model-free; used by the
|
|
unit tests so the wiring is verifiable with no model present.
|
|
- **`JouleclawRuntimeBackend`** (here) — runs the model **in-process on
|
|
JouleClaw's own from-scratch GGUF tensor runtime** (`jouleclaw-runtime`): no
|
|
external binary, no FFI. Tokenize → build the decode-step graph → compile
|
|
against the registered kernels (`jouleclaw-backend-reference` /
|
|
`-apple`) → autoregressive decode → detokenize, all in Rust the family wrote.
|
|
This is the **default** backend for the `run` binary.
|
|
- **`LlamaCompletionBackend`** (here) — drives a real on-device GGUF via
|
|
`llama-completion` (llama.cpp). Set `JOULE_CODE_L3_ENGINE=llama` to use it.
|
|
|
|
Either local path: no datacenter, no network. In production the cheapest capable
|
|
backend is chosen by `jouleclaw_dispatch::ModelBank`'s cost table.
|
|
|
|
## Run it against a local model
|
|
|
|
```bash
|
|
# Default: in-process on jouleclaw-runtime (the from-scratch tensor engine).
|
|
cargo run --release --bin run -- /path/to/model.gguf "Write a Rust function that reverses a string."
|
|
# or: JOULE_CODE_L3_MODEL=/path/to/model.gguf cargo run --release --bin run
|
|
|
|
# Use the llama.cpp shell-out backend instead:
|
|
JOULE_CODE_L3_ENGINE=llama cargo run --release --bin run -- /path/to/model.gguf "..."
|
|
```
|
|
|
|
The `run` binary generates with the local model, content-addresses the result as
|
|
a `CodingOp` (tier L3), and puts the energy of the inference on the receipt, read
|
|
as close to the work as the host allows and graded by how much of the inference
|
|
the reading covers:
|
|
|
|
- **In-process engine (default)** — metered on this process's own CPU and GPU
|
|
(`jouleclaw_energy::span::ProcessSpanMeter`): the CPU energy the kernel counts
|
|
for this process's threads, plus its GPU energy — an exact zero where it spent
|
|
no GPU time, the GPU rail where it had the GPU to itself. A span that covers the
|
|
whole inference is a hardware reading, `HwShunt` with `measured_uj` set. One
|
|
that covers part of it (the GPU shared with another process, so the CPU alone)
|
|
is an estimate of the whole, `Estimator`: the reading plus the host meter's
|
|
figure over the same window.
|
|
- **llama engine** — the subprocess is read on its own process tree
|
|
(`sandbox_meter::TreeMeter`) where the host counts each process's CPU energy
|
|
(macOS). That reading is never whole: it is the process's CPU only, not the GPU
|
|
the binary may offload to, and the process exits in its window, so its record
|
|
lacks its last time on a core — a lower bound. So it is always an estimate of
|
|
the whole, the reading plus the host meter's figure, never `HwShunt`.
|
|
- **No reading of the engine's own process** — no per-process counter, or, for
|
|
the in-process engine, this process's counters not live (the span then falls
|
|
back to a host meter, whose figure is the machine's, not the engine's): the
|
|
host meter's figure as it is. That is JouleClaw's IOReport counter where its
|
|
CPU energy rail is live (`HwShunt`); where it is not (macOS 27 for an
|
|
unprivileged process), the Mac's whole-machine power from the SMC, integrated
|
|
over time (`ModelBased`), or the load-average estimator where that is not live
|
|
either (`Estimator`); and the token-count estimate anywhere else (Linux and
|
|
Windows included; this binding builds `jouleclaw-energy` with the `ioreport`
|
|
and `smc` features only). An inference shorter than the host counter's minimum
|
|
window (2 s for the SMC) takes the token-count estimate too: the counter's
|
|
figure is noise below it.
|
|
|
|
The receipt has no coverage field, so a partial reading is never sealed as a
|
|
hardware reading of the whole. The printout names the engine, the host meter and
|
|
the IOReport status, and says how the figure was obtained.
|
|
|
|
Verified live: the llama.cpp path against a local 350M GGUF, and the in-process
|
|
from-scratch runtime against `ternary-bonsai-1.7b-q2_0` (qwen3, Q2_0) — real text
|
|
generated, a real `op_id`, the cascade closing at L3.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
cargo test # EchoBackend path — no model required
|
|
|
|
# In-process from-scratch runtime against a real GGUF (opt-in):
|
|
JOULE_BONSAI=/path/to/ternary-bonsai-1.7b-q2_0.gguf \
|
|
cargo test --release --test runtime_l3 -- --ignored --nocapture
|
|
```
|
|
|
|
The `run` binary's meter tests check each engine's reading against an oracle
|
|
sharing no code with the meter — the process's own energy count, read from
|
|
inside it — and assert it only where this host counts each process's energy
|
|
(Apple Silicon macOS, the per-process counter live); elsewhere they check the
|
|
fallback. CI tests this workspace on Linux only, where they take the fallback,
|
|
so the oracle assertions are not in CI and run only locally: run
|
|
`cargo test -- --nocapture` on an Apple Silicon Mac to see them.
|