open-standards/eoc/eoc-rs/crates/eoc-local/tests
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 15cf07c2ee eoc-local: a span is measured only when both reads succeed and the counter advanced, and inference that ran is never 0 µJ
722d80d9 listed two defects here as not done.

`llamacpp` took a failed first counter read as 0 (`unwrap_or(0)`), so the next
successful read reported the counter's whole since-boot total as the
generation's energy. Its drain then tagged that figure measured unless some
chunk had seen a zero delta. This is the defect 467acb38 fixed in
eoc-streaming. And `mlc` and `onnx` reported 0 µJ for inference that ran
whenever nothing was measured: tagged estimated, but still a zero presented as
a figure.

The rule now lives once, in a new `span` module, and every backend meters
through it:

- `MeterSpan::open` reads the counter; `reading` gives `Some(delta)` only when
  that opening read succeeded, the read now succeeds, and the counter advanced
  between them. A failed read at either end, a counter that held, one that
  went down, or the stub: `None`, never 0 and never a since-boot total.
- `span_cost` makes a reading `JouleCost::measured` and asks for the estimate
  only when there was none.
- `per_token_estimate` is tokens × `fallback_joules_per_token`, to the nearest
  µJ, and never 0: at least one token is counted, a coefficient that is not a
  finite positive number gives way to the default (0.05 J/token, mlx's figure
  since the start), and a product under half a µJ is 1 µJ.

Per backend:

- `llamacpp`: `TokenChunk::cumulative_microjoules` is now `Option<u64>`, the
  reading so far, `None` when nothing was measured. The inference's cost is
  the final chunk's reading, measured, or the per-token estimate over the
  tokens streamed. An earlier chunk's reading covers part of the generation,
  so it is never taken as the cost of the whole. The backend gains
  `fallback_joules_per_token` and its builder. Nothing outside this file uses
  `TokenChunk`.
- `mlc` and `onnx` gain `fallback_joules_per_token` (default 0.05 J) and its
  builder, and estimate over the placeholder's own token count, as `mlx` does.
- `mlx` already had the right rule; it now goes through the same functions, so
  a zero coefficient no longer yields 0 µJ.

Factored so it tests without native code. Neither the `llamacpp` nor the `mlx`
module binds a single symbol of its native crate (both generators are
placeholders), so this crate's tests compile them without their features
(`cfg(any(feature, test))`; mlx on macOS). Their metering now runs in every
`cargo test --workspace`, where before it ran in none: llama.cpp is not built
here, and `--features mlx` fails in mlx-sys's Metal kernel build on this
toolchain. The `not(feature = "llamacpp")` error stub, unreachable since the
module itself was gated on the feature, is gone.

Tests, all on a scripted counter that reads a list (`None` is a failed read)
and counts its reads:

- span: an advance of 450 µJ reads 450; a failed first read followed by
  9,000,000 reads nothing; so does a failed last read, a held counter, a
  counter that went down, and the stub. The estimate is 800,000 µJ for 8 × 0.1
  J, 100,000 for 0 tokens at 0.1 J, the default for 0, -1, NaN and infinity,
  1 µJ for 1e-12 J, and saturates at u64::MAX.
- llamacpp, through `stream` and `infer`: the stub gives the 4-token estimate
  (200,000 µJ, estimated) and no chunk a reading; a failed first read then 9 J
  gives no chunk a reading and the estimate; 1,000 -> 1,100 ... 1,450 gives
  chunk readings 100, 200, 300, 400 and a measured 450 over exactly six reads;
  a held counter and a failed last read give the estimate. A table test pins
  that only the final chunk's reading is a cost.
- mlc, onnx, mlx: measured 450 for an advance; the estimate (800,000 µJ) for a
  held counter, a failed first read and a failed last read; the stub's
  estimate, never 0. The `joule_attribution` integration test now asserts the
  mlc stub's estimate (500,000 µJ, estimated) where it asserted 0.

Falsified, each mutant applied to the uncommitted tree, run, and restored
byte-for-byte (sha256 checked):

- a failed start taken as 0 fails 5 tests (llamacpp, mlc, mlx, onnx, span),
  the llamacpp one with every chunk at `Some(9000000)`;
- `>=` in place of `>` (a held counter measured) fails 9;
- llamacpp reading the counter's absolute value per chunk (HEAD's rule with a
  0 baseline) fails 3;
- a partial chunk reading kept as the cost fails the tally test;
- mlc's and onnx's HEAD estimate of 0 fail 2 each;
- the estimate without its 1 µJ floor, without the one-token minimum, or
  without the coefficient check fails the estimate test.
One mutant survived and is equivalent: `final_reading = reading.or(previous)`
cannot differ, since nothing sets the field before the final chunk.

`cargo test -p eoc-local` passes (36 + 1), with `--features mlc,gguf` (47 +
3) and `--features onnx --lib` (43). The onnx leg runs `--lib` because the
`joule_attribution` test binary cannot link ort-sys without ONNX Runtime
installed when mlc is also enabled; that is unchanged. No clippy warning in
this crate, before or after.

Not done: the `llamacpp` error path still returns 0 µJ, estimated, with the
error text. Whether any work ran before an error is unknown there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:06:55 -04:00
..
joule_attribution.rs eoc-local: a span is measured only when both reads succeed and the counter advanced, and inference that ran is never 0 µJ 2026-09-30 21:06:55 -04:00