open-standards/wai/tools/quantum-meter/tests/hardware.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

149 lines
7.7 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.

//! The meter on this host's own counters and GPU, where it has them: a WebGPU
//! member's runs and the CPU reference's, each in a window long enough to
//! resolve, and one run that is not.
//!
//! This is its own test binary because the first meter in a process probes
//! the GPU counter with the load it is given, once for the process's life.
//!
//! On a host with no WebGPU adapter, or where the counters are not live, the
//! test says so and checks what holds there: every window is unmetered. On a
//! host where they are live it prints `quantum-meter hardware:` lines, which a
//! CI job that expects live counters looks for.
//!
//! A GPU window can lose its figure to another process using the GPU over it,
//! which a shared host cannot prevent; the test then requires the refusal to
//! say so, and no figure.
use quantum_meter::{Declared, MIN_GPU_WINDOW, ProcessRunMeter, Refused, label, probe_load, span_of_runs};
use std::time::Duration;
use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset};
use wai_quantum::quantum_energy::{EnergyClass, check_label};
use wai_quantum::quantum_fabric::{EnergyCalibration, RunMeter, pair_updates};
use wai_quantum::quantum_fabric_webgpu::blocking;
const DECLARED: Declared = Declared { counter_ppm: 50_000 };
/// Hadamards, a chain of controlled X, phases, Hadamards: `5n − 1` gates.
fn circuit(n: u8) -> Circuit {
let mut c = Circuit::with_gateset(n, Gateset::V2);
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
}
#[test]
fn a_members_runs_are_metered_over_a_window_the_counters_resolve() {
let discovery = blocking::discover().expect("discovery resolves natively");
let Some(gpu) = discovery.found.first() else {
eprintln!("quantum-meter: no WebGPU adapter on this host ({} refused)", discovery.refused.len());
return;
};
let wide = circuit(gpu.max_qubits().min(18));
let ops = pair_updates(&wide);
let mut meter = ProcessRunMeter::new(
probe_load(|| {
let _ = blocking::simulate_from(gpu, &wide, 0);
}),
DECLARED,
);
let on_gpu = || blocking::simulate_from(gpu, &wide, 0).map(|_| ops).map_err(|e| format!("{e:?}"));
let on_cpu = || wide.simulate_from(0).map(|_| ops).map_err(|e| format!("{e:?}"));
if !meter.is_live() {
eprintln!("quantum-meter: the counters are not live here (GPU counter {})", meter.gpu_liveness());
// No window opens, and a run through the member is unmetered.
assert!(span_of_runs(&mut meter, Duration::ZERO, on_gpu).is_err());
assert!(matches!(meter.last(), Some(Err(Refused::NotLive(_)))), "{:?}", meter.last());
let (_, joules_micro, class) = blocking::simulate_metered(gpu, &wide, 0, &mut meter).expect("the run itself");
assert_eq!((joules_micro, class), (0, None));
return;
}
// The member's runs, two seconds of them.
let runs = span_of_runs(&mut meter, MIN_GPU_WINDOW * 2, on_gpu).expect("the member runs");
assert!(runs.runs >= 1 && runs.ops == ops * runs.runs, "{runs:?}");
match meter.last().expect("a window was closed") {
Ok(span) => {
assert!(span.gpu_ns > 0, "the member's runs spent GPU time: {span:?}");
assert!(span.cpu_uj > 0, "and the host's side of them CPU energy: {span:?}");
let (joules_micro, class) = label(span, DECLARED).expect("two seconds of GPU work is labelled");
assert_eq!((runs.joules_micro, runs.class.clone()), (joules_micro, Some(class.clone())), "stop returned the span's label");
assert_eq!(joules_micro, span.cpu_uj + span.gpu_uj);
assert!(matches!(class, EnergyClass::OnChipCounter { .. }), "{class:?}");
assert_eq!(check_label(joules_micro, &class), Ok(()));
let mut calibration = EnergyCalibration::new();
calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref());
let (nanojoules, _) = calibration.finish().expect("a figure");
eprintln!(
"quantum-meter hardware: {} on {} qubits: {} runs in {:?}, cpu {} uJ + gpu {} uJ over {} ms of GPU time -> {nanojoules} nJ/op, {} ppm",
gpu.id(),
wide.n_qubits,
runs.runs,
runs.wall,
span.cpu_uj,
span.gpu_uj,
span.gpu_ns / 1_000_000,
class.uncertainty().expect("declared").relative_ppm
);
}
// The GPU was not this process's alone over the window, or its rail
// gave no reading: no figure, and the reason kept.
Err(why @ (Refused::NoGpuFigure(_) | Refused::Counter(_))) => {
assert_eq!((runs.joules_micro, runs.class.clone()), (0, None), "{why}");
eprintln!("quantum-meter hardware: {} window unmetered: {why}", gpu.id());
}
Err(other) => panic!("a two-second window of the member's runs was refused: {other}"),
}
// The CPU reference's runs, in the same domain: no GPU time, so the
// figure is the CPU count alone.
let runs = span_of_runs(&mut meter, Duration::from_secs(1), on_cpu).expect("the reference runs");
match meter.last().expect("a window was closed") {
Ok(span) => {
assert_eq!((span.gpu_ns, span.gpu_uj), (0, 0), "the reference spends no GPU time: {span:?}");
assert!(span.cpu_uj > 0, "{span:?}");
assert_eq!(runs.joules_micro, span.cpu_uj);
let class = runs.class.clone().expect("a second of CPU work is labelled");
assert_eq!(check_label(runs.joules_micro, &class), Ok(()));
let mut calibration = EnergyCalibration::new();
calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref());
let (nanojoules, _) = calibration.finish().expect("a figure");
eprintln!(
"quantum-meter hardware: the CPU reference on {} qubits: {} runs in {:?}, cpu {} uJ -> {nanojoules} nJ/op, {} ppm",
wide.n_qubits,
runs.runs,
runs.wall,
span.cpu_uj,
class.uncertainty().expect("declared").relative_ppm
);
}
// Another process's GPU client came or went over the window: the GPU
// counter then gives no figure, a zero included.
Err(why @ (Refused::NoGpuFigure(_) | Refused::Counter(_))) => {
assert_eq!((runs.joules_micro, runs.class.clone()), (0, None), "{why}");
eprintln!("quantum-meter hardware: the CPU reference's window unmetered: {why}");
}
Err(other) => panic!("a one-second window of the reference's runs was refused: {other}"),
}
// One run of the member, in the window the fabric opens around it: far
// under a second, so it is unmetered, never read at a resolution the rail
// does not have.
let small = circuit(6);
let (_, joules_micro, class) = blocking::simulate_metered(gpu, &small, 0, &mut meter).expect("the run itself");
assert_eq!((joules_micro, class), (0, None), "a run of {:?}", meter.last());
match meter.last().expect("the run's window was closed") {
Ok(span) => assert!(matches!(label(span, DECLARED), Err(Refused::TooShort { .. })), "{span:?}"),
Err(Refused::NoGpuFigure(_) | Refused::Counter(_)) => {}
Err(other) => panic!("{other}"),
}
// Every start was matched by a stop: the meter starts again.
assert!(meter.start().is_ok());
assert!(meter.stop().is_ok());
}