The family contract: measured when the platform exposes it, estimated when it doesn't, and the provenance always says which. This crate's energy paths price work as marginal draw, `ΔE_busy − P_idle·Δt`, on the IOReport counter, and three of them turned a counter that measured nothing into a figure that is then treated as measured. The defect (b9d7e945, unchanged since origin/main54e89b56): - fabric_dispatch::tune_shape_live read the counter with `.unwrap_or(0)` / `.unwrap_or(e0)`, clamped the marginal with `.max(0.0)` and truncated the per-op figure to u64. On a CPU rail that does not advance (macOS 27.0.1, both our Macs) every backend came out at 0 µJ per op, joules_minimal picked one, and TuningProfile::record kept it: a recorded 0 beats every later measurement, so the shape was pinned to that backend for the process. - examples/fabric_calibrate wrote ~/.jouleclaw/fabric-calibration.json, which jouleclaw-fabric loads as Measured placement costs, with idle `(e1 - e0) / t` (0 on a frozen rail) and each member's marginal clamped to `.max(0.0)`: zeros written as measurements. - examples/mpp_energy overlaid its matmul2d figure into the same file with an unclamped marginal (negative on a frozen rail), printed a CPU/GPU ratio of 0/0 = NaN, and ignored whether its CPU matmul ran at all.b9d7e945now declines the counter on those hosts, so today these paths stop at `is_available()`. That covers today's hosts. A counter that is available but does not see a window would still have written zeros. The fix. - New module marginal_energy, pure arithmetic over raw readings, which the three paths share: idle_draw_uw refuses an idle window that did not advance (an SoC draws power doing nothing, so a counter that holds is not metering), marginal_uj refuses a busy window at or below idle and is never clamped to zero, per_op_uj refuses a per-op cost that would round to 0, energy_ratio is None unless both figures are positive and finite, and refusal() renders what an example prints before it exits non-zero. - tune_shape_live: a failed read, an idle baseline that did not advance, or either backend's window drawing nothing above idle voids the tune (None, nothing recorded), through the new tune_measured: an unmeasured backend neither wins nor loses against a measured one. - TuningProfile::record (and so record_global and tune_and_record) never records a zero-joule cost; tune_and_record returns None for one. - fabric_calibrate and mpp_energy refuse to write, exit 1, and print the file, the reason and jouleclaw_energy::ioreport_status() when the counter is unavailable, the idle baseline did not advance, or any marginal is <= 0. They refuse before any write, so no partial file. mpp_energy's ratio is n/a unless both runs were measured, and its CPU run now asserts the matmul ran, as its GPU run already did (a refused shape would meter an empty loop, and that run is one of the refusal gates). - energy_autotune (example) says why a shape was not tuned, with the status. Observed, M5 Max and M3 Ultra (macOS 27.0.1), examples run with HOME set to a fresh directory and JOULECLAW_FABRIC_CALIBRATION unset: refusing to write <tmp>/.jouleclaw/fabric-calibration.json: the energy counter is not available on this host (apple-ioreport declined) IOReport status: CpuRailFrozen; nothing was written (its figures are loaded as measurements) exit 1 for fabric_calibrate and mpp_energy on both, no file under the temp HOME. The real ~/.jouleclaw/fabric-calibration.json kept its 2026-06-11 11:02:14 mtime and SHA-256 (792b00e6...) on the M5 Max, and its mtime on the M3 Ultra. energy_autotune: "not tuned: the meter produced no figure (IOReport CpuRailFrozen)", global profile empty. Tests. Default features, what CI's jouleclaw-rs job runs: 294 lib tests pass (the marginal_energy and energy_autotune tests below are not feature-gated). With `--features "rayon fabric-meter"`: 302 pass on the M5 Max and, from the same binary, on the M3 Ultra. - a_counter_that_holds_at_idle_has_no_idle_baseline, a_busy_window_at_or_below_ idle_is_refused_never_clamped_to_zero, a_per_op_cost_never_rounds_to_zero, the_cpu_gpu_ratio_is_undefined_unless_both_are_measured, a_refusal_names_the_file_the_reason_and_the_ioreport_status, a_refusal_says_why. - a_zero_joule_cost_is_never_recorded (incl. through record_global), tune_and_record_records_nothing_when_the_winner_drew_zero_joules, an_unmeasured_candidate_voids_the_tune. - a_live_tune_records_only_what_the_meter_measured (fabric-meter, on this host): calls tune_shape_live itself and asserts it records exactly what it returns, never a zero, and None where ioreport_status() is not Live. Both machines: "ioreport=CpuRailFrozen tuned=None recorded=None". Falsified, each mutation applied to this commit and restored with git checkout: - FL1, record accepts a zero: "the meter did not see the work: nothing to record". - FL2, a held idle counter as a 0 W baseline: "left: Ok(0.0) right: Err(IdleDidNotAdvance { secs: 3.0 })". - FL3, the marginal clamped to zero again: "left: Ok(0.0) right: Err(NoMarginal { marginal_uj: 0.0 })". - FL4, the raw ratio: "left: Some(NaN) right: None". - FL5, an unmeasured backend taken as 0 µJ: "left: Some(Cost { backend: Cpu, uj: 0, ns: 0 }) right: None". - FL6, tune_shape_live returning and recording a zero cost when the counter is declined: the host test fails, "a live tune records exactly what it returns", tuned=Some(Cost { uj: 0 .. }) recorded=None (the record gate dropped it; the test caught the returned zero). - FL7, the refusal without the IOReport status, and FL8, a per-op cost rounding to 0 accepted: their tests fail. clippy --no-deps --tests on the crate (with and without fabric-meter, plus the three energy examples): no warning in the lines this commit adds. The crate does not pass clippy on its own: two deny-level lints elsewhere (clippy::erasing_op at gemma4.rs:562 and :582, clippy::approx_constant in a lib test) stop it before the tests and examples, so they were checked with those two allowed. The pre-existing warnings in the touched files (mpp_energy's double_parens, fabric_calibrate's chunks_exact_mut, fabric_dispatch's too_many_arguments) are unchanged, and mpp_energy's unused-Result warning is gone. `cargo check --tests --examples` for aarch64-unknown-linux-gnu (CI's jouleclaw-rs target, with +fp16): clean. Not done here, deliberately: - jouleclaw-fabric loads every calibration row as Measured, and the file carries no date, OS or provenance. A file written on a live counter is still loaded at that grade on a host where the counter is now declined. - TuningProfile and the autotune/1 receipt carry no provenance field, so a sealed tuning profile cannot say how its joules were obtained. Both are wire or format changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
87 lines
3.5 KiB
Rust
87 lines
3.5 KiB
Rust
//! Live energy-autotune: for a couple of representative matmul shapes, measure the
|
||
//! CPU (AMX) path against the GPU path **under the IOReport meter**, pick the
|
||
//! joules-minimal backend, and print the resulting tuning profile.
|
||
//!
|
||
//! Run on a QUIET box for meaningful marginal-energy figures:
|
||
//! cargo run --release --features "rayon fabric-meter" --example energy_autotune
|
||
//!
|
||
//! The path being validated — meter → real kernels → joules-minimal selection →
|
||
//! global profile — executes regardless of box load; only the *accuracy* of the
|
||
//! µJ figures needs a quiet machine (the marginal-energy subtraction assumes a
|
||
//! stable idle draw). A backend the platform can't run for a shape reports
|
||
//! `u64::MAX` and simply never wins. A shape the meter cannot measure — the IOReport
|
||
//! counter unavailable (its CPU energy rail not advancing, among other reasons), or a
|
||
//! window that draws nothing above idle — is left untuned: nothing is recorded.
|
||
|
||
use jouleclaw_loader_gguf::energy_autotune;
|
||
use jouleclaw_loader_gguf::fabric_dispatch;
|
||
|
||
fn f32_to_bf16(v: f32) -> u16 {
|
||
let b = v.to_bits();
|
||
((b + (((b >> 16) & 1) + 0x7fff)) >> 16) as u16
|
||
}
|
||
|
||
fn main() {
|
||
// Decode (m = 1) and a prefill window (m = 256), same n×k projection — the two
|
||
// regimes whose joules-minimal backend can genuinely differ.
|
||
let shapes = [(1usize, 4096usize, 4096usize), (256, 4096, 4096)];
|
||
|
||
println!("tuning {} shape(s) live on the IOReport meter…\n", shapes.len());
|
||
for &(m, n, k) in &shapes {
|
||
let a: Vec<f32> = (0..m * k).map(|i| ((i % 101) as f32 / 50.0) - 1.0).collect();
|
||
let w: Vec<u8> = (0..n * k)
|
||
.flat_map(|i| f32_to_bf16(((i % 97) as f32 / 48.0 - 1.0) * 0.1).to_le_bytes())
|
||
.collect();
|
||
|
||
// ~1 s per backend past the counter's update window.
|
||
match fabric_dispatch::tune_shape_live(&a, &w, m, n, k, 1.0) {
|
||
Some(cost) => println!(
|
||
" m={m:<4} n={n} k={k} → {:>3} wins @ {:>10} µJ/op, {:>10} ns/op",
|
||
cost.backend.label(),
|
||
cost.uj,
|
||
cost.ns
|
||
),
|
||
None => println!(
|
||
" m={m:<4} n={n} k={k} → not tuned: the meter produced no figure (IOReport {:?})",
|
||
jouleclaw_energy::ioreport_status()
|
||
),
|
||
}
|
||
}
|
||
|
||
println!("\nglobal tuning profile (the dispatcher's lookup):");
|
||
let snapshot = energy_autotune::global_snapshot();
|
||
if snapshot.is_empty() {
|
||
println!(
|
||
" (empty — no shape was measured; IOReport {:?})",
|
||
jouleclaw_energy::ioreport_status()
|
||
);
|
||
}
|
||
for (key, cost) in snapshot {
|
||
println!(
|
||
" m_bucket={:<4} n={} k={} → {} ({} µJ/op)",
|
||
key.m_bucket,
|
||
key.n,
|
||
key.k,
|
||
cost.backend.label(),
|
||
cost.uj
|
||
);
|
||
}
|
||
// With the `attest` feature also on, seal the profile as a verifiable
|
||
// autotune/1 JCR-1 receipt — the tuning decisions become an artifact, not a claim.
|
||
#[cfg(feature = "attest")]
|
||
{
|
||
use ed25519_dalek::SigningKey;
|
||
let sk = SigningKey::from_bytes(&[0x5e; 32]);
|
||
match energy_autotune::seal_profile(&sk, "did:jouleclaw:autotune") {
|
||
Ok(env) => println!(
|
||
"\nsealed autotune/1 receipt: {} bytes — verifiable under the steward key",
|
||
env.len()
|
||
),
|
||
Err(e) => println!("\nseal failed: {e}"),
|
||
}
|
||
}
|
||
|
||
println!(
|
||
"\nrun the model with JOULECLAW_ENERGY_AUTOTUNE=1 to dispatch by this profile."
|
||
);
|
||
}
|