//! 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()); }