All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 57s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m42s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 55s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m32s
standards workspaces build + test / aac-rs (pull_request) Successful in 2m38s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 57s
standards workspaces build + test / map-rs (pull_request) Successful in 56s
standards workspaces build + test / mesh-rs (pull_request) Successful in 55s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m33s
standards workspaces build + test / openpay (pull_request) Successful in 3m27s
lockfiles / every Cargo.lock is current (pull_request) Successful in 37s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 7s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 6s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m14s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m1s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m35s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m12s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m13s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m18s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m14s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m24s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m59s
The README said only that joule cost is the unit of accounting. It now has the rule and where the cascade reads its meter since 0d78becb: around the neural stage, never around a hit, which is charged a per-hit figure calibrated on the host. The example output shows the `hit` line, with and without a live counter. eoc-cascade's own docs carry the final figures from the build host (M3 Ultra, macOS 27.0.1, release, load average 5 to 14, three runs of `examples/hit_cost`): - 10,000 cache hits: 4.18 to 4.20 ms, 0.375 µs a resolve at the median (parent: 1,641 to 1,651 ms, 162 to 164 µs). A cascade given no meter at all: 3.55 to 3.60 ms, 0.333 µs. KV 0.667 µs, graph 0.75 to 0.79 µs. - calibrated: 1,413 to 1,716 nJ a cache hit, 2,672 to 2,961 nJ a KV hit, 3,007 to 3,163 nJ a graph hit, each from 416 to 3,328 lookups in a span 51 to 351 times the 12 to 19 µJ an empty span read; 0.85 to 2.3 ms for a first hit, calibration included, 0.96 to 1.35 ms for one asked again. - a neural stage that does nothing: 162.4 to 162.6 µs a resolve and 14.3 to 14.8 µJ measured, the cascade's two reads; 0.79 to 0.83 µs with no meter. The three misses before the opening read: 0.95 µJ and 220 ns together. `HitCalibration`'s docs say the figure is that of the core the batch ran on and of the build, and that the calibration's own energy is charged to no resolve. The module docs no longer link the private `cascade_cost`. eoc-meter's crate docs and `detect_in_process` say how eoc-cascade uses a counter now (it was the example of a caller that should bracket a batch, not each operation). eoc-core's `JouleCost::zero` and eoc-cache said a lookup's energy is below any counter's resolution; it is about a microjoule, readable as the mean of a batch, and less than a read costs, which is why no counter is read for a hit. `examples/hit_cost` also runs the same stages with no meter, the baseline a metered hit is now within 0.05 µs of. Co-Authored-By: Claude Opus 5.5 <[email protected]>
103 lines
6.9 KiB
Markdown
103 lines
6.9 KiB
Markdown
# `eoc-rs` — EOC reference implementation in Rust
|
|
|
|
Apache-2.0 reference implementation of the **Energy-Optimized Compute** specification (CC-BY-4.0 spec lives in [`../spec/`](../spec/)).
|
|
|
|
EOC is a four-stage memoizing cascade. Every query is tried at each stage in turn and the first that can answer wins. The four stages, cheapest to most expensive, are:
|
|
|
|
1. **Cache** (`eoc-cache`) — content-addressed LRU. Cache hits are nearly free.
|
|
2. **Key-value** (`eoc-kv`) — exact key match plus cosine-similarity embedding match.
|
|
3. **Graph** (`eoc-graph`) — triple-store retrieval. Reference impl is a small in-memory matcher; production deployments swap in a real graph backend driven by DCY (EOC-4).
|
|
4. **Neural** (`eoc-neural`) — last-resort inference. Reference impl ships an `EchoBackend`; production deployments wire in llama.cpp / Ollama / Anthropic API / etc.
|
|
|
|
Joule cost is the unit of accounting. The `eoc-meter` crate reads cumulative micro-joules from whatever whole-work hardware counter is available and live (Linux RAPL, Apple Silicon IOReport; NVIDIA NVML behind the `cuda` feature is GPU telemetry, never the cascade's counter) and falls back to a `StubCounter` everywhere else — including WASM.
|
|
|
|
## What a resolve is charged
|
|
|
|
A cost is `Measured` only when every micro-joule of it was read from a live counter over the work it is booked to; anything else is `Estimated`. The cascade reads its meter where the meter can resolve the work, and nowhere else.
|
|
|
|
- **A resolve the neural stage answers is read.** Two reads of the meter bracket the stage, the first taken once the cache, the KV stage and the graph have missed. The span is combined with the cost the stage reported: measured when the stage estimated nothing, the larger of the two readings when the stage metered itself, an estimate when the stage estimated. The three misses are spent before the first read and are not in the figure; the meter's own closing cost is. On an M3 Ultra the misses together took 220 ns and 0.95 µJ, and a span over no work read 12 to 29 µJ, so the measured figure is not under the neural stage's energy.
|
|
- **A resolve the cache, the KV stage or the graph answers reads no meter**, and is never measured. It is charged the stage's own figure plus what a hit on that tier costs on this host. The first time a tier answers, the cascade repeats that lookup inside one span of the meter until the span reads fifty times what a span over no work does (or 40 ms have gone), and keeps the reading per lookup, in nanojoules. Every later hit on the tier is charged that, in whole micro-joules with the remainder carried to the next hit, so the micro-joules charged over any number of hits sum to the calibrated total. `Cascade::calibration` returns the figure, the batch, the span and the empty span, or why a tier has none; `Cascade::calibrate` calibrates again; `Cascade::without_hit_calibration` switches a tier off.
|
|
|
|
A hit is not bracketed because a read of a counter costs more than a hit. The in-process counter on macOS takes each read only once the reading thread is seen to have left its core, about 80 µs. With a span around every resolve, 10,000 cache hits took 1,647 ms on that host, 164 µs each, and each was sealed as a measured 13 to 28 µJ: the meter's own cost, the same as a span around a neural stage that does nothing. Without it they take 4.2 ms, 0.38 µs each, and are charged the 1.2 to 1.7 µJ a cache hit was calibrated at there (2.6 to 3.4 µJ for a KV or graph hit; the calibration takes 1 to 2.4 ms, once a tier), as an estimate: read by this counter on this host, but over a batch, and the mean of a batch is not a reading of any one hit.
|
|
|
|
With no live counter there is no calibration and no span, and every resolve keeps the cost its stage reported.
|
|
|
|
## Workspace layout
|
|
|
|
| Crate | Purpose |
|
|
|----------------|--------------------------------------------------------------|
|
|
| `eoc-core` | Types: `Query`, `Response`, `Stage`, `JouleCost`, `Receipt`. |
|
|
| `eoc-cache` | Stage 1 — LRU cache + the `Stage` trait. |
|
|
| `eoc-kv` | Stage 2 — KV + embedding-similarity lookup. |
|
|
| `eoc-graph` | Stage 3 — in-memory triple store. |
|
|
| `eoc-neural` | Stage 4 — neural inference trait + `EchoBackend`. |
|
|
| `eoc-meter` | Hardware energy counters with stub fallback. |
|
|
| `eoc-cascade` | Glue: walks the four stages, attributes joule cost. |
|
|
| `eoc` | Reference CLI. |
|
|
| `eoc-wasm` | `wasm-bindgen` wrapper — browser-runnable cascade. |
|
|
|
|
## Running
|
|
|
|
```sh
|
|
cargo test --workspace
|
|
cargo clippy --workspace --all-targets
|
|
cargo run --bin eoc -- query "what is 2+2"
|
|
cargo run --bin eoc -- meter
|
|
```
|
|
|
|
Example output, on a host with no live counter, where the cost is the stage's own figure:
|
|
|
|
```
|
|
$ cargo run --bin eoc -- query "what is 2+2"
|
|
stage : kv
|
|
payload: 4
|
|
cost : 50 µJ (estimated)
|
|
hit : the kv stage's own figure; no calibration by stub (the counter did not advance)
|
|
receipt: <64-char hex>
|
|
```
|
|
|
|
and on a Mac whose in-process counter is live (`--release`; the stage's 50 µJ plus the calibrated figure):
|
|
|
|
```
|
|
$ cargo run --release --bin eoc -- query "what is 2+2"
|
|
stage : kv
|
|
payload: 4
|
|
cost : 53 µJ (estimated)
|
|
hit : a kv hit is charged 3357 nJ on this host, on top of the stage's own figure: process-energy read 6876 µJ over 2048 lookups in one span (24 µJ over none)
|
|
receipt: <64-char hex>
|
|
```
|
|
|
|
`cargo run --release -p eoc-cascade --example hit_cost` prints what each tier's hit takes and is charged on the host it runs on.
|
|
|
|
## CUDA / NVML
|
|
|
|
NVIDIA NVML support is feature-gated and **off by default** so the workspace builds without `libnvidia-ml`. NVML's total-energy counter reads the GPU alone, so `detect()` never hands it to the cascade, whose costs carry no field to say a figure covers part of the work: `NvmlCounter` is GPU telemetry, read beside the whole-work counter, and it is constructed only while its counter is live and counts no faster than the board can draw. To enable:
|
|
|
|
```sh
|
|
cargo build -p eoc-meter --features cuda
|
|
```
|
|
|
|
## WASM
|
|
|
|
The cascade compiles to `wasm32-unknown-unknown`. The meter does not — RAPL, NVML, and `powermetrics` are all absent in the browser, so WASM builds use `StubCounter` and report estimated joule cost.
|
|
|
|
```sh
|
|
rustup target add wasm32-unknown-unknown
|
|
cargo build -p eoc-wasm --target wasm32-unknown-unknown --release
|
|
wasm-bindgen target/wasm32-unknown-unknown/release/eoc_wasm.wasm \
|
|
--out-dir ./pkg --target web
|
|
```
|
|
|
|
Then from JavaScript:
|
|
|
|
```js
|
|
import init, { WasmCascade } from "./pkg/eoc_wasm.js";
|
|
await init();
|
|
const cascade = new WasmCascade();
|
|
const r = await cascade.resolve("ping");
|
|
console.log(r); // { stage: "kv", payload: "pong", microjoules: 50, ... }
|
|
```
|
|
|
|
## License
|
|
|
|
Apache-2.0 for the Rust source in this workspace. The EOC specification text in [`../spec/`](../spec/) is CC-BY-4.0. See [`../LICENSE`](../LICENSE).
|