mesh: an in-process engine is metered by this process's own CPU and GPU energy #193

Merged
dcharlot merged 1 commit from mesh-meter/process-span into main 2026-10-09 02:41:26 -04:00
Owner

Mesh's headline figure, microjoules per accepted token, is meant to be measured at the meter. Until now every real serve through mesh was estimated:

  • the GGUF serving binaries were hardwired to EstimatorMeter;
  • HwMeter bridges the host meter, and on macOS 27 that is the estimator, because IOReport's CPU rails are frozen.

This PR makes an in-process serve a measured one wherever the counters can back it.

ProcessMeter (mesh-meter, behind hw-meter)

It is for an engine that runs in this process. Each serve is metered by jouleclaw's ProcessSpanMeter (#146): this process's own CPU energy plus its own GPU energy.

  • Whole span → its counters' grade (HwShunt for a live reading). Whole means the process was effectively the GPU's sole user (≥99% of GPU time) and used no Neural Engine. A process with no GPU time spent exactly 0 J on it, so CPU-only engines qualify.
  • Any other span → Estimator, as an estimate of the whole request, because mesh/1 has no coverage field (SPEC §3.3). That covers a shared GPU, a GPU with no figure, and declared ANE use. The figure is the reading plus EstimatorMeter for the rest of the host, the rule HwMeter already applies to a counter that reads part of a request.

Meter::open_span and Window. The default is None, so every existing meter keeps snapshot semantics. ProcessMeter grades over the whole span, because its GPU grade depends on the process's share across it. Session uses a span window where a meter keeps one; mesh-core's API is unchanged.

Serving binaries (mesh-engine-gguf, metal)

  • serve_falcon and serve_openai_falcon meter with ProcessMeter. The Metal liveness load comes from jouleclaw-backend-apple. Without metal they keep the estimator.
  • New serve_gemma_metered serves Gemma 4 through mesh with ProcessMeter and prints each sealed receipt.

Build fix: mesh-engine-gguf under gguf did not compile on main

ServeOutcome and ServeRequest gained a decision field. Three initializers in mesh-engine-gguf lacked it, and nothing noticed because CI never built that feature. They now set decision: None, since a token-generating engine answers no typed decision. standards-build's mesh-rs leg now runs cargo test -p mesh-engine-gguf --features gguf, through the feature_tests hook, so this cannot recur silently.

Measured (M5 Max, macOS 27)

Serve Receipt Figure Per accepted token
ReferenceEngine (mesh-core tests/process_meter.rs) HwShunt 1,051,668 µJ 4,108 µJ
Gemma 4 E2B on CPU, request 0 (serve_gemma_metered) HwShunt 12.52 J 0.78 J
Gemma 4 E2B on CPU, request 1 HwShunt 12.16 J 0.76 J
Gemma 4 E2B on CPU, request 2 HwShunt 12.54 J 0.78 J

The test's figure lies within the 1,090,946 µJ of this process's CPU energy the test read around the serve, through its own proc_pid_rusage oracle. Every envelope verifies.

Verified

  • mesh-meter + mesh-core with hw-meter-apple: 17 tests passed.
  • cargo test --workspace: 52 passed.
  • cargo test -p mesh-engine-gguf --features gguf: passes.
  • cargo check -p mesh-engine-gguf --features metal --bins: clean.
  • Mutation caught: billing a whole span's figure plus the rest-of-host estimate fails both the unit test and the end-to-end bracket ("the receipt read 55691805 µJ, more than this process's 966177 µJ of CPU energy").

🤖 Generated with Claude Code

Mesh's headline figure, **microjoules per accepted token**, is meant to be measured at the meter. Until now every real serve through mesh was estimated: - the GGUF serving binaries were hardwired to `EstimatorMeter`; - `HwMeter` bridges the host meter, and on macOS 27 that is the estimator, because IOReport's CPU rails are frozen. This PR makes an in-process serve a measured one wherever the counters can back it. ## `ProcessMeter` (mesh-meter, behind `hw-meter`) It is for an engine that runs in this process. Each serve is metered by jouleclaw's `ProcessSpanMeter` (#146): this process's own CPU energy plus its own GPU energy. - **Whole span → its counters' grade** (`HwShunt` for a live reading). Whole means the process was effectively the GPU's sole user (≥99% of GPU time) and used no Neural Engine. A process with no GPU time spent exactly 0 J on it, so **CPU-only engines qualify**. - **Any other span → `Estimator`**, as an estimate of the whole request, because `mesh/1` has no coverage field (SPEC §3.3). That covers a shared GPU, a GPU with no figure, and declared ANE use. The figure is the reading plus `EstimatorMeter` for the rest of the host, the rule `HwMeter` already applies to a counter that reads part of a request. **`Meter::open_span` and `Window`.** The default is `None`, so every existing meter keeps snapshot semantics. `ProcessMeter` grades over the whole span, because its GPU grade depends on the process's share across it. `Session` uses a span window where a meter keeps one; mesh-core's API is unchanged. ## Serving binaries (mesh-engine-gguf, `metal`) - `serve_falcon` and `serve_openai_falcon` meter with `ProcessMeter`. The Metal liveness load comes from `jouleclaw-backend-apple`. Without `metal` they keep the estimator. - New **`serve_gemma_metered`** serves Gemma 4 through mesh with `ProcessMeter` and prints each sealed receipt. ## Build fix: mesh-engine-gguf under `gguf` did not compile on `main` `ServeOutcome` and `ServeRequest` gained a `decision` field. Three initializers in mesh-engine-gguf lacked it, and nothing noticed because CI never built that feature. They now set `decision: None`, since a token-generating engine answers no typed decision. standards-build's mesh-rs leg now runs `cargo test -p mesh-engine-gguf --features gguf`, through the `feature_tests` hook, so this cannot recur silently. ## Measured (M5 Max, macOS 27) | Serve | Receipt | Figure | Per accepted token | |---|---|---|---| | `ReferenceEngine` (mesh-core `tests/process_meter.rs`) | **HwShunt** | 1,051,668 µJ | 4,108 µJ | | Gemma 4 E2B on CPU, request 0 (`serve_gemma_metered`) | **HwShunt** | 12.52 J | 0.78 J | | Gemma 4 E2B on CPU, request 1 | **HwShunt** | 12.16 J | 0.76 J | | Gemma 4 E2B on CPU, request 2 | **HwShunt** | 12.54 J | 0.78 J | The test's figure lies within the 1,090,946 µJ of this process's CPU energy the test read around the serve, through its own `proc_pid_rusage` oracle. Every envelope verifies. ## Verified - mesh-meter + mesh-core with `hw-meter-apple`: 17 tests passed. - `cargo test --workspace`: 52 passed. - `cargo test -p mesh-engine-gguf --features gguf`: passes. - `cargo check -p mesh-engine-gguf --features metal --bins`: clean. - **Mutation caught:** billing a whole span's figure plus the rest-of-host estimate fails both the unit test and the end-to-end bracket ("the receipt read 55691805 µJ, more than this process's 966177 µJ of CPU energy"). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mesh: an in-process engine is metered by this process's own CPU and GPU energy
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 7m16s
standards workspaces build + test / aac-rs (pull_request) Successful in 6m34s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 7m20s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 7m37s
standards workspaces build + test / map-rs (pull_request) Successful in 5m17s
standards workspaces build + test / mesh-rs (pull_request) Successful in 2m8s
standards workspaces build + test / openpay (pull_request) Successful in 13m7s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 5s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 5s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 5s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m25s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m32s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m55s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m27s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m27s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m33s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 3m4s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m24s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 5m30s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 5s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m45s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m51s
1fb670a512
Mesh's headline figure, microjoules per accepted token, is to be measured
at the meter. Yet every real serve through mesh was estimated: the GGUF
serving binaries were hardwired to EstimatorMeter, and HwMeter bridges
the host meter, which on macOS 27 is the estimator (IOReport's CPU rails
are frozen).

- mesh-meter `ProcessMeter`, behind `hw-meter`, for an engine that runs
  in this process. Each serve is metered by jouleclaw's ProcessSpanMeter:
  this process's own CPU energy plus its own GPU energy.
  - A span that covers the whole request is graded as its counters
    grade it: HwShunt for a live reading. Whole means the process was
    effectively the GPU's sole user and used no Neural Engine; a
    process with no GPU time spent exactly 0 J on it, so CPU-only
    engines qualify.
  - Any other span is an estimate of the whole request, Estimator: the
    reading plus EstimatorMeter for the rest of the host, as HwMeter
    bills a counter that reads part of a request. `mesh/1` has no
    coverage field.
- `Meter::open_span` and `Window`. The default is None, so existing
  meters keep snapshot semantics. ProcessMeter grades over the whole
  span, because its GPU grade depends on the process's share over it.
  Session uses a span window where a meter keeps one.
- mesh-engine-gguf, with `metal`: serve_falcon and serve_openai_falcon
  meter with ProcessMeter (the Metal liveness load comes from
  jouleclaw-backend-apple). New `serve_gemma_metered` serves Gemma 4
  through mesh with ProcessMeter.
- Fixes mesh-engine-gguf under `gguf`: it no longer compiled on main.
  ServeOutcome and ServeRequest gained a `decision` field and three
  initializers lacked it, unseen because CI never built the feature.
  standards-build now runs `cargo test -p mesh-engine-gguf --features
  gguf` on the mesh-rs leg.

Measured on an M5 Max:
- mesh-core tests/process_meter.rs: a ReferenceEngine serve sealed
  HwShunt 1,051,668 µJ (4,108 µJ per accepted token), within the
  1,090,946 µJ of this process's CPU energy the test read around the
  serve.
- serve_gemma_metered, Gemma 4 E2B on CPU, 3 x 16 tokens: HwShunt
  12.52 J, 12.16 J and 12.54 J (0.78, 0.76 and 0.78 J per accepted
  token). Every envelope verifies.

Mutation caught: billing a whole span's figure plus the rest-of-host
estimate fails the unit test and the end-to-end bracket ("the receipt
read 55691805 µJ, more than this process's 966177 µJ").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!193
No description provided.