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
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]>
149 lines
7.7 KiB
Rust
149 lines
7.7 KiB
Rust
//! 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());
|
||
}
|