open-standards/wai/tools/quantum-meter/src/main.rs
Transaction Science 8606de265c
All checks were successful
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 10m31s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 50s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m32s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m25s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m20s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 57s
standards workspaces build + test / aac-rs (pull_request) Successful in 2m29s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 2m20s
standards workspaces build + test / map-rs (pull_request) Successful in 56s
standards workspaces build + test / mesh-rs (pull_request) Successful in 53s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m36s
standards workspaces build + test / openpay (pull_request) Successful in 2m54s
lockfiles / every Cargo.lock is current (pull_request) Successful in 40s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m9s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m1s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m10s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m12s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m19s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m7s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m49s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m5s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m5s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
quantum-meter: the GPU probe's load runs for 50 ms each call, not one run
The first meter in a process probes the GPU counter with a load, and the
probe needs a few tens of milliseconds of this process's GPU work between
its samples. One 18-qubit run of the member, about 2 ms on an M4 Max,
passed there by margin; on the CI Mac (M3 Ultra), whose GPU counter the
energy crate's own test finds live under a real burn, it was judged
Frozen, and every window was rightly unmetered.

probe_load wraps one run of a member so each call repeats it until 50 ms
have passed. The tool and the device test use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 02:48:54 -04:00

215 lines
9.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Calibrates the fabric's CPU reference and a WebGPU member with this
//! process's own energy counters, and places a circuit by the figures.
//!
//! quantum-meter --counter-ppm <n> [--qubits <n>] [--seconds <s>] [--gateset 1|2] [--expect <adapter key>]
//!
//! `--counter-ppm` is the operator's evaluation of the counters' own relative
//! uncertainty, in parts per million. The platform publishes none, so there
//! is no default.
//!
//! One circuit on `--qubits` qubits (20) is run by each member, again and
//! again inside one window of `--seconds` seconds (3) at least: a window long
//! enough for the counters to resolve, where a single run is not. Each
//! member's line gives the window's readings and the figure they earn, in
//! nanojoules per operation (one pair of amplitudes under one gate), or says
//! why the window has none. With a figure on both, the last line is the member
//! the fabric places that circuit on.
//!
//! Exit 0 when it ran, with or without figures; 1 when a member could not be
//! found or run; 2 for a bad argument.
use quantum_meter::{Declared, ProcessRunMeter, Refused, Runs, Span, label, probe_load, span_of_runs};
use std::process::ExitCode;
use std::time::Duration;
use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset};
use wai_quantum::quantum_energy::EnergyClass;
use wai_quantum::quantum_fabric::{EnergyCalibration, FABRIC_DEFAULT, Member, Parity, REFERENCE, apply_kernel, detect_local, pair_updates, place_for};
use wai_quantum::quantum_fabric_webgpu::blocking;
struct Args {
declared: Declared,
qubits: u8,
at_least: Duration,
gateset: Gateset,
expect: Option<String>,
}
fn usage() -> ExitCode {
eprintln!("usage: quantum-meter --counter-ppm <n> [--qubits <n>] [--seconds <s>] [--gateset 1|2] [--expect <adapter key>]");
ExitCode::from(2)
}
fn args() -> Option<Args> {
let (mut ppm, mut qubits, mut seconds, mut gateset, mut expect) = (None, 20u8, 3u64, Gateset::V2, None);
let mut it = std::env::args().skip(1);
while let Some(flag) = it.next() {
let value = it.next()?;
match flag.as_str() {
"--counter-ppm" => ppm = Some(value.parse::<u32>().ok().filter(|&p| p > 0)?),
"--qubits" => qubits = value.parse::<u8>().ok().filter(|n| (2..=24).contains(n))?,
"--seconds" => seconds = value.parse::<u64>().ok().filter(|s| (1..=600).contains(s))?,
"--gateset" => {
gateset = match value.as_str() {
"1" => Gateset::V1,
"2" => Gateset::V2,
_ => return None,
}
}
"--expect" => expect = Some(value),
_ => return None,
}
}
Some(Args { declared: Declared { counter_ppm: ppm? }, qubits, at_least: Duration::from_secs(seconds), gateset, expect })
}
/// The circuit every window runs: on each qubit a Hadamard, then a chain of
/// controlled X, then T and a dyadic phase on each, then a Hadamard on each.
/// Valid in either gateset; `5n − 1` gates.
fn circuit(n: u8, gs: Gateset) -> Circuit {
let mut c = Circuit::with_gateset(n, gs);
let one = |base, target, param| Gate { base, controls: Vec::new(), target, param };
(0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0)));
(0..n - 1).for_each(|q| c.ops.push(Gate { base: BaseGate::X, controls: vec![q], target: q + 1, param: 0 }));
for q in 0..n {
c.ops.push(one(BaseGate::T, q, 0));
c.ops.push(one(BaseGate::P, q, 1 + u16::from(q % 6)));
}
(0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0)));
c
}
/// One member's window as a line, and the figure it earns.
fn report(member: &str, kernel: &str, runs: &Runs, last: Option<&Result<Span, Refused>>, declared: Declared) -> Option<(u64, EnergyClass)> {
let mut calibration = EnergyCalibration::new();
calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref());
let figure = calibration.finish();
let read = match last {
Some(Ok(s)) => format!("cpu={} uJ gpu={} uJ gpu_time={} ms", s.cpu_uj, s.gpu_uj, s.gpu_ns / 1_000_000),
_ => "no readings".to_string(),
};
let earned = match (&figure, last) {
(Some((nanojoules, class)), _) => {
let ppm = class.uncertainty().map_or(0, |u| u.relative_ppm);
format!("{nanojoules} nJ/op, {}, {ppm} ppm", class.label())
}
(None, Some(Err(why))) => format!("unmetered: {why}"),
// Read, and still no figure: the labelling rule says why, or the
// fabric's resolution floor refused the label.
(None, Some(Ok(s))) => match label(s, declared) {
Err(why) => format!("unmetered: {why}"),
Ok(_) => "unmetered: the reading is below the resolution floor".to_string(),
},
(None, None) => "unmetered".to_string(),
};
println!("quantum-meter: {member} kernel={kernel} runs={} ops={} wall={} ms {read} -> {earned}", runs.runs, runs.ops, runs.wall.as_millis());
figure
}
fn main() -> ExitCode {
let Some(args) = args() else { return usage() };
let kernel = apply_kernel(args.gateset);
let wide = circuit(args.qubits, args.gateset);
let ops = pair_updates(&wide);
println!("quantum-meter: circuit qubits={} gates={} ops={ops} kernel={kernel}", wide.n_qubits, wide.ops.len());
let discovery = match blocking::discover() {
Ok(d) => d,
Err(e) => {
eprintln!("quantum-meter: discovery failed: {e:?}");
return ExitCode::FAILURE;
}
};
for r in &discovery.refused {
println!("quantum-meter: adapter {} ({}) is not a member: {:?}", r.key, r.adapter, r.error);
}
let found = match &args.expect {
Some(key) => discovery.found.iter().find(|m| m.key() == key),
None => discovery.found.first(),
};
let Some(gpu) = found else {
eprintln!("quantum-meter: no WebGPU member{}", args.expect.as_ref().map(|k| format!(" with key {k}")).unwrap_or_default());
return ExitCode::FAILURE;
};
println!("quantum-meter: adapter {} member={} holds {} qubits", gpu.key(), gpu.id(), gpu.max_qubits());
// The first meter in a process probes the GPU counter with this load: a
// run of the member it will meter.
let mut meter = ProcessRunMeter::new(
probe_load(|| {
let _ = blocking::simulate_from(gpu, &wide, 0);
}),
args.declared,
);
println!("quantum-meter: counters live={} gpu={}", meter.is_live(), meter.gpu_liveness());
let mut gpu_member = match blocking::member(gpu, FABRIC_DEFAULT) {
Ok(m) => m,
Err(e) => {
eprintln!("quantum-meter: {} could not be certified: {e:?}", gpu.id());
return ExitCode::FAILURE;
}
};
let mut cpu_member = detect_local();
// An unmetered window still runs, so each member is exercised either way.
let window = |meter: &mut ProcessRunMeter, run: &mut dyn FnMut() -> Result<u64, String>| match span_of_runs(meter, args.at_least, &mut *run) {
Ok(runs) => Ok(runs),
// The meter would not start: run once without it.
Err(_) if !meter.is_live() => run().map(|ops| Runs { runs: 1, ops, joules_micro: 0, class: None, wall: Duration::ZERO }),
Err(e) => Err(e),
};
let gpu_runs = window(&mut meter, &mut || {
let state = blocking::simulate_from(gpu, &wide, 0).map_err(|e| format!("{e:?}"))?;
std::hint::black_box(state);
Ok(ops)
});
let gpu_runs = match gpu_runs {
Ok(r) => r,
Err(e) => {
eprintln!("quantum-meter: {} failed its run: {e}", gpu.id());
return ExitCode::FAILURE;
}
};
let gpu_figure = report(&gpu_member.id, kernel, &gpu_runs, meter.last(), args.declared);
let cpu_runs = window(&mut meter, &mut || {
let state = wide.simulate_from(0).map_err(|e| format!("{e:?}"))?;
std::hint::black_box(state);
Ok(ops)
});
let cpu_runs = match cpu_runs {
Ok(r) => r,
Err(e) => {
eprintln!("quantum-meter: the CPU reference failed its run: {e}");
return ExitCode::FAILURE;
}
};
let cpu_figure = report(&cpu_member.id, kernel, &cpu_runs, meter.last(), args.declared);
// Figures go on both members or on neither: a figure on one alone would
// place it whatever it costs.
let (Some(gpu_figure), Some(cpu_figure)) = (gpu_figure, cpu_figure) else {
println!("quantum-meter: no placement by energy: a figure is needed on both members");
return ExitCode::SUCCESS;
};
let set = |member: &mut Member, implementation: &str, figure: &(u64, EnergyClass)| {
member.kernels.iter_mut().filter(|k| k.kernel == kernel && k.implementation == implementation).for_each(|k| k.energy = Some(figure.clone()));
};
set(&mut gpu_member, gpu.implementation(), &gpu_figure);
set(&mut cpu_member, REFERENCE, &cpu_figure);
let members = [cpu_member, gpu_member];
let predict = |_: &Member, k: &wai_quantum::quantum_fabric::KernelEntry| k.energy.as_ref().map(|(nanojoules, _)| nanojoules.saturating_mul(ops).saturating_mul(1_000));
match place_for(&members, kernel, &Parity::BitExact, predict) {
Some((member, entry)) => println!(
"quantum-meter: placed {} ({}) for {} qubits: {} nJ/op against {} nJ/op on the other member",
member.id,
entry.implementation,
wide.n_qubits,
entry.energy.as_ref().map_or(0, |e| e.0),
if member.id == members[0].id { gpu_figure.0 } else { cpu_figure.0 }
),
None => println!("quantum-meter: no member is certified for {kernel}"),
}
ExitCode::SUCCESS
}