open-standards/eoc/eoc-rs
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science ec45aaaca9 toolchain: Rust 1.98.0 -> 1.99.1 (change-controlled pin)
Approved by the owner on 2026-10-22. Procedure: change `channel`, run
`rustup show`, then run the pipeline.

What changed
- rust-toolchain.toml and eoc/eoc-rs/rust-toolchain.toml: channel "1.98.0"
  becomes "1.99.1". These are the only two pins in the tree, and the root one
  governs every other cargo root.
- 64 workflows under .github/workflows (the forge's CI): every `Install Rust
  1.98.0`, `rustup toolchain install / default / component add / target add
  --toolchain 1.98.0` and `--default-toolchain 1.98.0` now names 1.99.1
  (409 lines). Two of those lines are in build steps rather than install
  steps (wai-conformance.yml's two `wasm32 (unix)` steps each
  `rustup target add --toolchain`), and they move with the rest: a target
  added to a toolchain that is not installed is an error.
- Left as they are, because they record what was measured on 1.98.0:
  standards-build.yml's "Verified on a native arm64 Linux container (rustc
  1.98.0)", wai-conformance.yml's note on Rust 1.98's LLD 22, and the
  "rustc 1.98.0 (example)" strings in receipts, vectors and docs. The
  one-codegen-unit workaround that note explains also stays. 1.99.1 ships
  LLD 23.1.1, which the note says links those inputs, but removing the
  workaround should wait for a run at the CI path on x86_64.

Why
- 1.98.0 has a P-critical miscompile, rust-lang/rust#161441 (I-miscompile,
  regression-from-stable-to-stable). rustc emits a vacant (null) method slot
  in a trait-object vtable, so a safe dynamic call jumps to address zero
  (undefined behaviour). RELEASES.md records the fix in 1.98.1 ("rustc: fix
  miscompilation in generating vtables").
- 1.99.1 is the current stable (channel manifest dated 2026-10-15; rustc
  58cf8f9c3 2026-10-13). It also fixes 1.99.0's own regressions: wcslen on
  UEFI and embedded targets, x86_64 FFI `bool` returns, and the rust-lld rpath
  on macOS.

Evidence
All of it ran on the build host ([email protected], aarch64-apple-darwin)
through ~/kani-logs/remote/rb. Nothing was built on the dev Mac.

- `rustup show`: the active toolchain is 1.99.1-aarch64-apple-darwin,
  "overridden by .../rust-toolchain.toml" (and by eoc/eoc-rs's own file
  there). rustc 1.99.1 (58cf8f9c3 2026-10-13), LLVM 23.1.1, clippy 0.1.99,
  rustfmt 1.10.0. The baseline is rustc 1.98.0 (88d9e12ae 2026-08-18).
- Scope: every cargo root the pin governs, which is 46 of the 49 tracked
  lockfiles. The other three: two are vendored crates that resolve inside
  their parents, and wai/wai-rs/fuzz builds on its own pinned nightly.
- Trees. The final tree is main at 96117c570 plus these two commits. Main
  moved four times while this ran, and each root's evidence comes from the
  newest tree it was run on:
  - 13 roots on the final tree: every root main changed after 1a465f23d,
    and every root that consumes such a crate by path.
  - 12 roots on 1a465f23d plus these two commits.
  - 7 roots on b16816671 plus these two commits.
  - 14 roots on aa61ab3b9 plus the pin change.
  For each root in the last three groups, checked with git diff and the
  lockfiles' path packages: none of its files, and no crate it reaches by
  path, differs on the final tree.
- Each root got:
  (a) `cargo fmt --all -- --check` on the pin and on 1.98.0. The output is
      byte-identical in all 46. CI gates no root on rustfmt, and none is
      rustfmt-clean on main.
  (b) `cargo clippy --locked --keep-going --workspace --all-targets` on both
      toolchains, with the diagnostics compared line by line. After the
      previous commit, no root has a diagnostic that only 1.99.1 reports.
      openpay's one differing line is the same lint at the same site,
      reworded.
  (c) `cargo test --workspace --locked`.
  (d) the root's other CI steps that can run on this host: feature-gated
      tests, conformance suites, release-profile tests, the aac page-set
      checks, and every clippy -D warnings gate. The gates are
      standards-build's macOS meter and omni lints (jouleclaw-energy x2,
      jouleclaw-omni metal, jouleclaw-loader-gguf, jcp, proof, eoc, sandbox),
      wai-pack, wai-quantum-cli (default, no-default and metal),
      wai-quantum-service, wai-quantum-qpu and wai-quantum-desktop.
- Totals: `cargo test --workspace` ran in 43 roots: 12,532 passed, 1 failed,
  101 ignored. The other 3 roots are wasm32-only, and their
  `cargo build --release --target wasm32-unknown-unknown` passed. Every
  other CI step passed (62 of 62).
- On the final tree: scripts/check-lockfiles.sh (cargo 1.99.1) found all 47
  lockfiles current, and scripts/check-advisories.py passes.
- tokio is 1.54.1 in every lockfile cargo reads. That is patched for both
  RUSTSEC-2026-0386 (>= 1.53.3) and RUSTSEC-2026-0384 (>= 1.54.1). The 1.53.1
  in vendor/wtransport/Cargo.lock is the upstream file kept with the vendored
  source; cargo resolves wtransport inside its parents, and both gates skip
  that file.
- What is not green, each one reproduced on 1.98.0:
  - wai-ops-gpu: under the debug profile, `corpus` trips the debug_assert at
    exec.rs:187 on both toolchains. CI runs it in release only, and that
    passes (6/6).
  - wai-ops: clippy::derived_hash_with_manual_eq (deny-by-default) on both
    toolchains. No CI job runs clippy there.
  - wai-vector/wasm: `--all-targets` cannot build the lib-test target (its
    own `panic_impl`) on either toolchain. `clippy --lib` for wasm32 reports
    0 diagnostics on both.
  - eoc-cascade's hit_calibration_process_energy test reads real energy
    counters, and it fails some of the time on the loaded shared host. In
    interleaved runs each toolchain passed 18 of 20. The full eoc-rs suite
    passed on the final tree (741 passed).
- Environment, not code:
  - wai-quantum-desktop embeds the Studio's build, so wai/quantum-studio was
    built first (`npm ci`, `node build.mjs`), as CI does.
- Not covered here: the x86_64 and arm64 Linux and Windows legs; the GPU,
  NPU, FPGA, emulator and hardware-meter legs; wasm32 tests under Node,
  wasmtime and wasip2; and the fuzz job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 17:42:20 -05:00
..
crates Cargo: no minimum-supported-version floor, because the policy is latest stable 2026-11-03 17:42:20 -05:00
Cargo.lock sec: answer the gate's first catch — two constant-time advisories, and pin an allowance to the version it excuses 2026-10-24 20:06:05 -04:00
Cargo.toml Cargo: no minimum-supported-version floor, because the policy is latest stable 2026-11-03 17:42:20 -05:00
LICENSE licences: ship the Apache-2.0 text the code already declares 2026-10-09 17:22:32 -04:00
README.md eoc docs: what a resolve is charged, with the measured figures; the example prints the unmetered baseline 2026-10-13 02:22:58 -04:00
rust-toolchain.toml toolchain: Rust 1.98.0 -> 1.99.1 (change-controlled pin) 2026-11-03 17:42:20 -05:00

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/).

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

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:

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.

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:

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/ is CC-BY-4.0. See ../LICENSE.