open-standards/wai/wai-quantum/tests/webgpu_member.rs
Transaction Science 1665baee8f
Some checks failed
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 10m7s
standards workspaces build + test / jouleclaw-rs (pull_request) Failing after 2m41s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m25s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 58s
standards workspaces build + test / map-rs (pull_request) Successful in 56s
standards workspaces build + test / mesh-rs (pull_request) Successful in 52s
standards workspaces build + test / aac-rs (pull_request) Successful in 6m29s
standards workspaces build + test / openpay (pull_request) Successful in 2m48s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m7s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 1m56s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 7m11s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 43s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 3m59s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 5m58s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 17s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m24s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 1m44s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m28s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 4s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m10s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m31s
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 1m20s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m28s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m12s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m59s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 8m44s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 32s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 54s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m10s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m11s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m36s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 5s
wai-pack / wai-pack · macos (pull_request) Successful in 1m5s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m11s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 2m44s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 1m47s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 17m12s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m50s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 5s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m15s
wai-pack / wai-pack · linux (pull_request) Successful in 1m18s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 24s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 3m59s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m17s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m47s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m9s
wai-quantum fabric: the review of the energy stage, fixed
An independent review found no wrong figure, pairing or fallback in the
arithmetic; it found four things the code did not hold.

- A receipt's member was whatever Member the caller paired with a runner:
  nothing compared them, so a member paired with another adapter's runner gave a
  signed, verifying receipt for work it did not do. A Runner now states the
  member it is (member_id), and Fabric::run refuses a pairing whose ids differ
  as a defect, before anything runs.
- "Figures on both members or on neither" was documented, not held: a
  calibration that earns no figure on one member (its readings under the
  resolution floor, say) left the other placed first whatever it cost. place is
  unchanged; place_for and Fabric now read figures on only some candidates as
  figures on none.
- Tests: the assertion that only placeable candidates are predicted held for
  any implementation, and is now a closure that panics on an unplaceable one
  over a list that holds two; a runner that hands back a refused label, or a
  label with no figure, is sealed as unmetered; the device test runs the tile
  implementation metered and on the fabric.
- The member's detail said a window "covers device work only"; it also holds
  the host's waits and copies between submissions, and says so.

Also: a calibration whose uncertainty no longer fits its field once the rounding
is added earns no figure rather than a capped one; ready() drives Fabric::run
natively without an executor; &R is a Runner and &mut M a RunMeter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 15:38:07 -04:00

946 lines
50 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 fabric's WebGPU member on the adapters of the machine that runs this.
//!
//! The machine declares the adapters it must have in
//! `WAI_QUANTUM_WEBGPU_EXPECT`: their keys, comma-separated and sorted, or
//! `none`. Every test that needs a member fails unless the adapters found are
//! exactly the declared ones, so a machine whose GPU went missing, or a run
//! that declared nothing, fails instead of passing over no member. Each test
//! prints what it established and, when `WAI_QUANTUM_WEBGPU_REPORT` names a
//! file, appends the same lines to it, which a CI step can count per adapter.
//!
//! ```text
//! WAI_QUANTUM_WEBGPU_EXPECT=gpu-metal-0 \
//! cargo test --features quantum_fabric_webgpu --test webgpu_member -- --test-threads=1
//! ```
//!
//! `WAI_QUANTUM_WEBGPU_WIDE=1` adds the widest states a GPU member holds.
#![cfg(not(target_arch = "wasm32"))]
use std::future::Future;
use std::io::Write;
use std::path::Path;
use std::sync::OnceLock;
use std::time::Instant;
use wai_quantum::ed25519_dalek::SigningKey;
use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset, MAX_QUBITS, QuantumError, from_wqc};
use wai_quantum::quantum_fabric::lanes::{self, GateWords, Lane};
use wai_quantum::quantum_fabric::plan::{self, Config, PlannedGate, Program, Step};
use wai_quantum::quantum_fabric::wgsl::{self, Detector, MUTANTS, Mutant, TILE_MUTANTS};
use wai_quantum::quantum_energy::{CounterFiltering, EnergyClass, Uncertainty};
use wai_quantum::quantum_fabric::{
Corpus, CpuReference, EnergyCalibration, FABRIC_DEFAULT, Fabric, FabricClass, FabricReceipt, Member, MemberError, Parity, Presence, REFERENCE, Refused, RunMeter, apply_kernel, detect_local, pair_updates, place,
};
use wai_quantum::quantum_fabric_webgpu::{Discovery, WebGpuError, WebGpuMember, blocking, probe_pairs};
/// The CPU's corpus digests: random-v1 (seed 7, 200 cases) and fabric-v1
/// (seed 7, 200 + 24 cases), per gateset. A bit-exact member reproduces them.
const PINS: [(Gateset, &str, &str); 2] = [
(Gateset::V1, "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36", "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7"),
(Gateset::V2, "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2", "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809"),
];
const RANDOM_V1: Corpus = Corpus::Random { seed: 7, cases: 200 };
static DISCOVERY: OnceLock<Discovery> = OnceLock::new();
fn discovery() -> &'static Discovery {
DISCOVERY.get_or_init(|| blocking::discover().expect("discovery resolves in one poll"))
}
/// Prints `line` and appends it to the file `WAI_QUANTUM_WEBGPU_REPORT` names.
fn report(line: &str) {
println!("{line}");
if let Some(path) = std::env::var_os("WAI_QUANTUM_WEBGPU_REPORT") {
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
writeln!(f, "{line}").expect("the report line is written");
}
}
/// The declared adapter set, as written, if it is exactly what was found;
/// otherwise why not, with what was found and refused and how to declare it.
fn declared(d: &Discovery) -> Result<String, String> {
let mut found: Vec<&str> = d.found.iter().map(|m| m.key()).collect();
found.sort_unstable();
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
let refused: Vec<String> = d.refused.iter().map(|r| format!("{} ({:?}): {}", r.key, r.adapter, r.error)).collect();
let refused_list = if refused.is_empty() { "none".to_string() } else { refused.join("; ") };
let how = format!("found: {found_list}; refused: {refused_list}. Declare the adapters this machine must have: WAI_QUANTUM_WEBGPU_EXPECT={found_list}");
let expect = match std::env::var("WAI_QUANTUM_WEBGPU_EXPECT") {
Ok(v) if !v.is_empty() => v,
_ => return Err(format!("WAI_QUANTUM_WEBGPU_EXPECT is not set. {how}")),
};
let keys: Vec<&str> = if expect == "none" { Vec::new() } else { expect.split(',').collect() };
let well_formed = keys.windows(2).all(|w| w[0] <= w[1]) && keys.iter().all(|k| !k.is_empty() && *k != "none" && k.trim() == *k);
if !well_formed {
return Err(format!("WAI_QUANTUM_WEBGPU_EXPECT={expect:?} is not a sorted, comma-separated list of keys, nor none. {how}"));
}
for k in &keys {
let (want, have) = (keys.iter().filter(|x| *x == k).count(), found.iter().filter(|x| *x == k).count());
if have < want
&& let Some(r) = d.refused.iter().find(|r| r.key == *k)
{
return Err(format!("the declared adapter {k} was refused: {}. {how}", r.error));
}
}
if found != keys {
return Err(format!("the adapters found are not the declared ones ({expect}). {how}"));
}
Ok(expect)
}
/// The members, once the declared adapters are exactly the ones found.
fn members() -> &'static [WebGpuMember] {
let d = discovery();
if let Err(why) = declared(d) {
panic!("{why}");
}
&d.found
}
/// A member call's result; every call resolves in one poll.
fn attempt<T>(f: impl Future<Output = Result<T, WebGpuError>>) -> Result<T, WebGpuError> {
blocking::resolve(f).expect("a native call resolves in one poll")
}
/// A member call that must succeed.
fn run<T>(f: impl Future<Output = Result<T, WebGpuError>>) -> T {
attempt(f).unwrap_or_else(|e| panic!("the member refused: {e}"))
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn next(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// A value in `[-bound, bound]`.
fn within(s: &mut u64, bound: i64) -> i32 {
((next(s) % (2 * bound as u64 + 1)) as i64 - bound) as i32
}
/// A random circuit of `ops` gates on `n` qubits with the wide corpus's gate
/// mix: every base gate, up to three distinct controls, `P(1..=32)`.
fn random_circuit(n: u8, ops: usize, seed: u64, gs: Gateset) -> Circuit {
let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
let mut s = seed;
let mut c = Circuit::with_gateset(n, gs);
for _ in 0..ops {
let base = bases[(next(&mut s) % bases.len() as u64) as usize];
let target = (next(&mut s) % u64::from(n)) as u8;
let mut controls = Vec::new();
for _ in 0..next(&mut s) % 4 {
let q = (next(&mut s) % u64::from(n)) as u8;
if q != target && !controls.contains(&q) {
controls.push(q);
}
}
let param = if base == BaseGate::P { 1 + (next(&mut s) % 32) as u16 } else { 0 };
c.ops.push(Gate { base, controls, target, param });
}
c
}
/// Five circuits the native simulator refuses, in each gateset.
fn refused_circuits() -> Vec<Circuit> {
let gate = |base, controls: Vec<u8>, target, param| Gate { base, controls, target, param };
let bad = [
(1, vec![gate(BaseGate::H, vec![], 0, 0), gate(BaseGate::P, vec![], 0, 0)]),
(1, vec![gate(BaseGate::P, vec![], 0, 33)]),
(1, vec![gate(BaseGate::X, vec![], 3, 0)]),
(2, vec![gate(BaseGate::X, vec![0], 0, 0)]),
(2, vec![gate(BaseGate::X, vec![5], 1, 0)]),
];
let mut out = Vec::new();
for gs in Gateset::ALL {
for (n, ops) in bad.clone() {
let mut c = Circuit::with_gateset(n, gs);
c.ops = ops;
out.push(c);
}
}
out
}
/// `Z` on qubit 0: on `[(0, 0), (i32::MIN, 0)]` its exact result is `2^31`,
/// which leaves 32 bits.
fn z_words() -> GateWords {
lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2)
}
const Z_STATE: [Lane; 2] = [[0, 0], [i32::MIN, 0]];
#[test]
fn g0_adapters_are_declared() {
let d = discovery();
for r in &d.refused {
println!("webgpu refused: key={} adapter={:?}: {}", r.key, r.adapter, r.error);
}
let expect = declared(d).unwrap_or_else(|why| panic!("{why}"));
for m in &d.found {
assert_eq!(m.implementation(), wgsl::implementation_id());
assert_eq!((m.identity().member_id(), m.identity().key()), (m.id().to_string(), m.key().to_string()));
assert_eq!(m.class(), m.identity().class());
let prefix = if m.class() == FabricClass::Cpu { "local/swgpu-" } else { "local/gpu-" };
assert!(m.id().starts_with(prefix) && m.id() > "local/cpu-zzzz", "{}", m.id());
assert!((25..=MAX_QUBITS).contains(&m.max_qubits()), "{}: {}", m.key(), m.max_qubits());
assert!(m.detail().ends_with("probe passed in this process"), "{}", m.detail());
}
report(&format!("webgpu adapters: as declared ({expect})"));
for m in &d.found {
report(&format!(
"webgpu member: key={} id={} class={} backend={} max_qubits={} implementation={}",
m.key(),
m.id(),
m.class().label(),
m.backend(),
m.max_qubits(),
m.implementation()
));
}
}
#[test]
fn g1_probe_matches_lanes() {
let pairs = probe_pairs(1 << 20, 11);
let expected: Vec<((u32, u32), (u32, u32))> = pairs.iter().map(|&(a, b)| (lanes::mul_wide(a, b), lanes::fxmul_wide(a, b))).collect();
for m in members() {
// A slot the device never wrote keeps its sentinel, which no correct
// result can equal under both.
for sentinel in [0xA5A5_A5A5u32, 0x5A5A_5A5A] {
let got = run(m.probe_with_sentinel(&pairs, sentinel));
assert_eq!(got.len(), pairs.len(), "{}", m.key());
if let Some(k) = (0..pairs.len()).find(|&k| got[k] != expected[k]) {
panic!("{}, sentinel {sentinel:#x}: {} × {} gives {:x?}, the CPU {:x?}", m.key(), pairs[k].0, pairs[k].1, got[k], expected[k]);
}
}
report(&format!("webgpu probe: key={} ok", m.key()));
}
}
#[test]
fn g2_kernel_matches_run_words() {
for m in members() {
let mut s = 0x6b65_726e_656c_u64;
let (mut flagged, mut clean) = (0, 0);
for case in 0..2000 {
let n = 1 + (next(&mut s) % 12) as u32;
// Three regimes: the whole 32-bit range (nearly always out of
// range), small states and entries (never), and states and
// entries up to 2^30 (either).
let mode = case % 3;
let value = |s: &mut u64| -> i32 {
match mode {
0 => next(s) as i32,
1 => within(s, 1 << 22),
_ => within(s, 1 << 30),
}
};
let entry = |s: &mut u64| -> u32 {
match mode {
0 => next(s) as u32,
1 => within(s, 1 << 29) as u32,
_ => within(s, 1 << 30) as u32,
}
};
let state: Vec<Lane> = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
let gates: Vec<GateWords> = (0..1 + next(&mut s) % 8)
.map(|_| {
let t = (next(&mut s) % u64::from(n)) as u32;
let ctrl = (0..n).filter(|&b| b != t && next(&mut s) % 4 == 0).fold(0u32, |acc, b| acc | 1 << b);
let mut w = [0u32; 8];
w.iter_mut().for_each(|x| *x = entry(&mut s));
GateWords { m: w, tbit: 1 << t, ctrl }
})
.collect();
let want = lanes::run_words(&state, &gates).expect("valid shapes");
let got = run(m.run_lanes(&state, &gates));
assert_eq!(got.out_of_range, want.out_of_range, "{}: case {case}, n = {n}", m.key());
assert!(got.amps == want.amps, "{}: case {case}, n = {n}: the lanes differ", m.key());
if want.out_of_range.is_some() {
flagged += 1;
} else {
clean += 1;
}
}
assert!(flagged >= 100 && clean >= 100, "{}: {flagged} flagged, {clean} clean", m.key());
report(&format!("webgpu kernel: key={} ok", m.key()));
}
}
#[test]
fn g3_certificates_reproduce_the_pins() {
let ms = members();
for (gs, random_pin, fabric_pin) in PINS {
let cpu = lanes::certify_corpus(gs, "local/test", RANDOM_V1);
assert_eq!(hex(&cpu.corpus), random_pin, "{gs:?}");
let other = Corpus::Random { seed: 8, cases: 300 };
let cpu_other = lanes::certify_corpus(gs, "local/test", other);
for m in ms {
for (corpus, cases, pin) in [(RANDOM_V1, 200, random_pin), (FABRIC_DEFAULT, 224, fabric_pin)] {
let c = blocking::certify(m, gs, corpus).unwrap_or_else(|e| panic!("{} {gs:?} {}: {e}", m.key(), corpus.name()));
assert_eq!(c.parity, Parity::BitExact, "{} {gs:?} {}", m.key(), corpus.name());
assert_eq!(c.cases, cases);
assert_eq!(hex(&c.corpus), pin, "{} {gs:?} {}", m.key(), corpus.name());
assert_eq!((c.kernel.as_str(), c.implementation.as_str(), c.member.as_str()), (apply_kernel(gs), m.implementation(), m.id()));
assert_eq!(c.generator, corpus.generator(gs));
if corpus == RANDOM_V1 {
assert_eq!(c.corpus, cpu.corpus);
}
report(&format!(
"webgpu certified: key={} member={} kernel={} corpus={} implementation={} parity=bit-exact cases={} digest={}",
m.key(),
m.id(),
c.kernel,
corpus.name(),
c.implementation,
c.cases,
hex(&c.corpus)
));
}
// A corpus with no pin: the member and the CPU agree on it.
let c = blocking::certify(m, gs, other).unwrap_or_else(|e| panic!("{} {gs:?} seed 8: {e}", m.key()));
assert_eq!((c.parity, c.cases), (Parity::BitExact, 300), "{} {gs:?}", m.key());
assert_eq!(c.corpus, cpu_other.corpus, "{} {gs:?}", m.key());
}
}
}
#[test]
fn g4_forced_geometry_gives_the_same_digests() {
for m in members() {
// One workgroup to a row (many rows), and three (a ragged last row).
for max_x in [1, 3] {
let forced = m.with_max_x(max_x);
for (gs, pin, _) in PINS {
let c = run(forced.certify(gs, RANDOM_V1));
assert_eq!(c.parity, Parity::BitExact, "{} max_x {max_x} {gs:?}", m.key());
assert_eq!(hex(&c.corpus), pin, "{} max_x {max_x} {gs:?}", m.key());
}
}
// Submissions and readback in small pieces. A ring of three records,
// rewritten between submissions of at most three gates, and state
// planes read back twelve bytes at a time: the smallest plane (two
// qubits) is sixteen bytes, so every plane takes more than one piece,
// the last one short.
let small = m.with_ring_records(3).with_readback_chunk(12);
for (gs, pin, _) in PINS {
let c = run(small.certify(gs, RANDOM_V1));
assert_eq!(c.parity, Parity::BitExact, "{} small pieces {gs:?}", m.key());
assert_eq!(hex(&c.corpus), pin, "{} small pieces {gs:?}", m.key());
}
// fabric-v1 through a ring of two, read back in pieces of 12 KiB and
// four bytes, so that the wide circuits' planes come back in many
// pieces, the last one short.
let pieces = m.with_ring_records(2).with_readback_chunk(12 * 1024 + 4);
for (gs, _, pin) in PINS {
let c = run(pieces.certify(gs, FABRIC_DEFAULT));
assert_eq!(c.parity, Parity::BitExact, "{} fabric-v1 in pieces {gs:?}", m.key());
assert_eq!(hex(&c.corpus), pin, "{} fabric-v1 in pieces {gs:?}", m.key());
}
report(&format!("webgpu geometry: key={} ok", m.key()));
}
}
#[test]
fn g5_conformance_vectors() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance");
for m in members() {
let mut total = 0;
for (dir, manifest) in [("vectors", "expected_hashes.json"), ("v2/vectors", "v2/expected_hashes.json")] {
let text = std::fs::read_to_string(root.join(manifest)).expect("the manifest reads");
let manifest: serde_json::Value = serde_json::from_str(&text).expect("the manifest parses");
let mut files: Vec<_> = std::fs::read_dir(root.join(dir)).expect("the vectors list").map(|e| e.expect("an entry").path()).filter(|p| p.extension().is_some_and(|x| x == "wqc")).collect();
files.sort();
assert!(!files.is_empty(), "no vectors in {dir}");
for path in files {
let name = path.file_name().unwrap().to_str().unwrap().to_string();
let entry = manifest.get(&name).unwrap_or_else(|| panic!("{dir}/{name} is not in its manifest"));
let (c, _) = from_wqc(&std::fs::read(&path).unwrap()).unwrap_or_else(|e| panic!("{dir}/{name}: {e}"));
let state = run(m.simulate(&c));
assert_eq!(Some(hex(&state.statevector_hash()).as_str()), entry["statevector_hash"].as_str(), "{} {dir}/{name}", m.key());
if let Some(measure) = entry.get("measure") {
let (seed, shots) = (measure["seed"].as_u64().unwrap(), measure["shots"].as_u64().unwrap());
assert_eq!(Some(hex(&state.histogram_hash(seed, shots)).as_str()), measure["histogram_hash"].as_str(), "{} {dir}/{name} histogram", m.key());
}
total += 1;
}
}
report(&format!("webgpu vectors: key={} {total} ok", m.key()));
}
}
#[test]
fn g6_wide_circuits_match_native() {
let wide = std::env::var("WAI_QUANTUM_WEBGPU_WIDE").is_ok_and(|v| v == "1");
for m in members() {
let mut sizes = vec![12u8, 16, 20];
if wide {
if m.class() == FabricClass::Gpu {
sizes.push(24);
if m.max_qubits() > 24 {
sizes.push(m.max_qubits());
}
} else {
report(&format!("webgpu wide: key={} software adapter: wide sizes not run", m.key()));
}
}
for n in sizes {
let qft = Circuit::qft(n);
let random = random_circuit(n, 200, 0x77_6964_65 + u64::from(n), Gateset::V2);
for (c, start) in [(&qft, 0usize), (&random, 0), (&random, (1usize << n) + 5)] {
let native = c.simulate_from(start).unwrap().statevector_hash();
let member = blocking::simulate_from(m, c, start).unwrap_or_else(|e| panic!("{} n = {n}: {e}", m.key()));
assert_eq!(hex(&member.statevector_hash()), hex(&native), "{} n = {n} start {start}", m.key());
}
report(&format!("webgpu wide: key={} n={n} matches native", m.key()));
}
// Long circuits on few qubits: more gates than one submission holds,
// so the ring is rewritten between submissions; and the same through a
// ring of three records.
for gs in Gateset::ALL {
let long = random_circuit(4, 5000, 0x6c6f_6e67, gs);
let native = hex(&long.simulate().unwrap().statevector_hash());
for member in [m.clone(), m.with_ring_records(3)] {
let got = blocking::simulate_from(&member, &long, 0).unwrap_or_else(|e| panic!("{} long {gs:?}: {e}", m.key()));
assert_eq!(hex(&got.statevector_hash()), native, "{} long {gs:?} {member:?}", m.key());
}
}
// The first op whose result leaves 32 bits sits in a later submission
// than the first, and a later op leaves them again: the report names
// the first op's place in the op-log, not in its submission.
let identity = lanes::gate_words(&Gate { base: BaseGate::I, controls: vec![], target: 0, param: 0 }, Gateset::V2);
for (member, first, second) in [(m.clone(), 4100usize, 4500usize), (m.with_ring_records(3), 4, 7)] {
let gates: Vec<GateWords> = (0..5000).map(|k| if k == first || k == second { z_words() } else { identity }).collect();
let want = lanes::run_words(&Z_STATE, &gates).expect("valid shapes");
assert_eq!(want.out_of_range, Some(first as u32));
assert_eq!(run(member.run_lanes(&Z_STATE, &gates)), want, "{} first out of range at {first}", m.key());
}
report(&format!("webgpu long: key={} matches native", m.key()));
}
}
#[test]
fn g7_refusals_are_typed() {
for m in members() {
for c in refused_circuits() {
let native = c.simulate_from(0).unwrap_err();
assert_eq!(attempt(m.simulate_from(&c, 0)).unwrap_err(), WebGpuError::Circuit(native), "{c:?}");
}
assert_eq!(attempt(m.simulate(&Circuit::new(MAX_QUBITS + 1))).unwrap_err(), WebGpuError::Circuit(QuantumError::TooManyQubits(MAX_QUBITS + 1)));
if m.max_qubits() < MAX_QUBITS {
let n = m.max_qubits() + 1;
assert_eq!(attempt(m.simulate(&Circuit::new(n))).unwrap_err(), WebGpuError::TooWide { n_qubits: n, max: m.max_qubits() });
}
// Past the member's width, whatever the core allows.
let wide = Program { n_qubits: m.max_qubits() + 1, start: 0, gates: Vec::new(), steps: Vec::new() };
assert_eq!(attempt(m.run_program(&wide)).unwrap_err(), WebGpuError::TooWide { n_qubits: m.max_qubits() + 1, max: m.max_qubits() });
// A result that leaves 32 bits is reported at its op, and the lane
// holds its low 32 bits.
let r = run(m.run_lanes(&Z_STATE, &[z_words()]));
assert_eq!(r.out_of_range, Some(0), "{}", m.key());
assert_eq!(r.amps[1][0] as u32, 0x8000_0000);
assert_eq!(attempt(m.simulate_from(&Circuit::new(1), 0)).map(|s| s.amps.len()), Ok(2));
// A malformed record, and a state the host cannot lay out, are host
// defects, never results.
let bad = GateWords { tbit: 3, ..z_words() };
assert_eq!(attempt(m.run_lanes(&[[0, 0]; 4], &[bad])).unwrap_err(), WebGpuError::Internal(4));
assert_eq!(attempt(m.run_lanes(&[[0, 0]; 3], &[z_words()])).unwrap_err(), WebGpuError::Internal(2));
report(&format!("webgpu refusals: key={} typed", m.key()));
}
}
/// Whether a member running `mutant`'s text is caught by the mutant's
/// detector.
fn caught(mm: &WebGpuMember, mutant: &Mutant, key: &str) -> bool {
match mutant.detector {
Detector::Probe => {
let edges = probe_pairs(0, 0);
let out = run(mm.probe(&edges));
let differing = edges.iter().zip(&out).filter(|&(&(a, b), o)| *o != (lanes::mul_wide(a, b), lanes::fxmul_wide(a, b))).count();
println!("webgpu mutant {} on {key}: {differing} of 484 edge pairs differ", mutant.name);
differing >= 1
}
Detector::Certificate => Gateset::ALL.into_iter().all(|gs| match attempt(mm.certify(gs, RANDOM_V1)) {
Err(e) => {
println!("webgpu mutant {} on {key}: {gs:?} certification refused: {e}", mutant.name);
true
}
Ok(c) => c.parity != Parity::BitExact,
}),
Detector::Range => attempt(mm.run_lanes(&Z_STATE, &[z_words()])).map(|r| r.out_of_range) != Ok(Some(0)),
Detector::Race => {
// A race need not show on every run: three tries per size.
let mut differing = Vec::new();
for n in [10u8, 11] {
let (state, words) = wgsl::race_witness(n);
let want = lanes::run_words(&state, &words).expect("valid shapes");
for _ in 0..3 {
match attempt(mm.run_lanes(&state, &words)) {
Ok(got) => differing.push(got.amps.iter().zip(&want.amps).filter(|(a, b)| a != b).count() + usize::from(got.out_of_range != want.out_of_range)),
Err(e) => {
println!("webgpu mutant {} on {key}: n = {n} refused: {e}", mutant.name);
differing.push(usize::MAX);
}
}
}
}
println!("webgpu mutant {} on {key}: lanes differing per run of the race witness: {differing:?}", mutant.name);
differing.iter().any(|&d| d > 0)
}
Detector::Malformed => attempt(mm.run_program(&wgsl::malformed_tile_run())) != Err(WebGpuError::Internal(4)),
}
}
#[test]
fn g8_mutants_are_killed() {
assert_eq!(probe_pairs(0, 0).len(), 484);
for m in members() {
let mut killed = 0;
for mutant in &MUTANTS {
let mm = attempt(m.with_shader(&wgsl::mutate(mutant))).unwrap_or_else(|e| panic!("{}: mutant {} does not compile: {e}", m.key(), mutant.name));
assert_eq!(mm.implementation(), "mutant");
assert!(caught(&mm, mutant, m.key()), "{}: mutant {} survived", m.key(), mutant.name);
killed += 1;
}
assert_eq!(killed, MUTANTS.len());
report(&format!("webgpu mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
// The tile kernel's mutants, on the tile member. First what the
// detectors rely on: the kernel as written reports the range, refuses
// the malformed run and runs the race witness as the CPU does.
let tiled = run(m.tiled());
assert_eq!(run(tiled.run_lanes(&Z_STATE, &[z_words()])).out_of_range, Some(0), "{}", m.key());
assert_eq!(attempt(tiled.run_program(&wgsl::malformed_tile_run())).unwrap_err(), WebGpuError::Internal(4), "{}", m.key());
for n in [10u8, 11] {
let (state, words) = wgsl::race_witness(n);
assert_eq!(run(tiled.run_lanes(&state, &words)), lanes::run_words(&state, &words).unwrap(), "{} race witness n = {n}", m.key());
}
let mut killed = 0;
for mutant in &TILE_MUTANTS {
let mm = attempt(tiled.with_shader(&wgsl::mutate_tile(mutant))).unwrap_or_else(|e| panic!("{}: tile mutant {} does not compile: {e}", m.key(), mutant.name));
assert_eq!((mm.implementation(), mm.config()), ("mutant", Config::TILED));
assert!(caught(&mm, mutant, m.key()), "{}: tile mutant {} survived", m.key(), mutant.name);
killed += 1;
}
assert_eq!(killed, TILE_MUTANTS.len());
report(&format!("webgpu tile mutants: key={} killed {killed} of {}", m.key(), TILE_MUTANTS.len()));
}
}
#[test]
fn g9_receipt_and_placement() {
for m in members() {
let member = blocking::member(m, FABRIC_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!((member.id.as_str(), member.class, member.presence), (m.id(), m.class(), Presence::Detected));
assert_eq!(member.kernels.len(), 2);
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
assert_eq!((entry.kernel.as_str(), entry.implementation.as_str()), (apply_kernel(gs), m.implementation()));
assert!(entry.energy.is_none());
assert_eq!(entry.certificate.parity, Parity::BitExact, "{gs:?}");
assert_eq!(entry.certificate.member, m.id());
}
// A receipt over a member's run.
let qft = Circuit::qft(5);
let state = blocking::simulate_from(m, &qft, 0).unwrap();
assert_eq!(state, qft.simulate().unwrap());
let entry = member.kernels.iter().find(|k| k.kernel == apply_kernel(qft.gateset)).unwrap();
let signer = SigningKey::from_bytes(&[7; 32]);
let receipt = FabricReceipt::seal(&signer, "local/test", state.statevector_hash(), &member, entry, 0, None);
assert!(receipt.verify());
assert!(receipt.certificate_matches(&entry.certificate));
let mut moved = receipt.clone();
moved.member_id.push('x');
assert!(!moved.verify());
// Placement: beside the CPU reference, with no energy figure, the
// reference; alone, the member.
let local = detect_local();
for gs in Gateset::ALL {
let both = [local.clone(), member.clone()];
let (placed, k) = place(&both, apply_kernel(gs), &Parity::BitExact).unwrap();
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (local.id.as_str(), REFERENCE));
let (placed, k) = place(std::slice::from_ref(&member), apply_kernel(gs), &Parity::BitExact).unwrap();
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (m.id(), m.implementation()));
}
report(&format!("webgpu receipt: key={} ok", m.key()));
}
}
#[test]
fn g10_repeated_runs_agree() {
let c = random_circuit(18, 200, 0x7265_7065_6174, Gateset::V1);
let native = hex(&c.simulate().unwrap().statevector_hash());
for m in members() {
let hashes: Vec<String> = (0..5).map(|_| hex(&blocking::simulate_from(m, &c, 0).unwrap().statevector_hash())).collect();
assert!(hashes.iter().all(|h| *h == native), "{}: {hashes:?}, native {native}", m.key());
report(&format!("webgpu repeated: key={} 5 runs, one hash", m.key()));
}
}
#[test]
fn g11_native_futures_resolve_in_one_poll() {
// A future that is still pending after one poll is an error, never a spin.
assert_eq!(blocking::resolve(std::future::pending::<()>()), Err(WebGpuError::Pending));
assert_eq!(blocking::resolve(std::future::ready(7)), Ok(7));
// Every other test reached its results through `blocking::resolve`.
report("webgpu futures: one poll each");
}
/// The milliseconds `f` takes, at best over `tries`, and its last result.
fn timed<T>(tries: usize, mut f: impl FnMut() -> T) -> (f64, T) {
let mut best = f64::INFINITY;
let mut last = None;
for _ in 0..tries {
let t = Instant::now();
let v = f();
best = best.min(t.elapsed().as_secs_f64() * 1e3);
last = Some(v);
}
(best, last.expect("at least one try"))
}
#[test]
fn g12_tiles_match_per_gate() {
for m in members() {
let tiled = run(m.tiled());
assert_eq!((tiled.id(), tiled.implementation(), tiled.config()), (m.id(), wgsl::tile_implementation_id().as_str(), Config::TILED));
assert!(tiled.implementation().starts_with("wgsl-lanes32-tile/"));
// Any state and any words, mapped through tile steps: the tile member
// equals the per-gate member and the CPU, lanes and report.
let mut s = 0x7469_6c65_6432_u64;
let (mut flagged, mut clean, mut runs) = (0, 0, 0);
for case in 0..2000 {
let n = 1 + (next(&mut s) % 12) as u32;
let full = case % 2 == 0;
let value = |s: &mut u64| -> i32 { if full { next(s) as i32 } else { within(s, 1 << 22) } };
let entry = |s: &mut u64| -> u32 { if full { next(s) as u32 } else { within(s, 1 << 29) as u32 } };
let state: Vec<Lane> = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
let gates: Vec<GateWords> = (0..1 + next(&mut s) % 24)
.map(|_| {
let t = (next(&mut s) % u64::from(n)) as u32;
let ctrl = (0..n).filter(|&b| b != t && next(&mut s) % 4 == 0).fold(0u32, |acc, b| acc | 1 << b);
let mut w = [0u32; 8];
w.iter_mut().for_each(|x| *x = entry(&mut s));
// Half the gates diagonal, on every target.
if next(&mut s) % 2 == 0 {
w[2..6].fill(0);
}
GateWords { m: w, tbit: 1 << t, ctrl }
})
.collect();
let planned: Vec<PlannedGate> = gates.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, k as u32)).collect();
if plan::schedule(n as u8, 0, planned, &Config::TILED).steps.iter().any(|st| matches!(st, Step::Tile { count, .. } if *count > 1)) {
runs += 1;
}
let want = lanes::run_words(&state, &gates).expect("valid shapes");
let got = run(tiled.run_lanes(&state, &gates));
assert_eq!(got.out_of_range, want.out_of_range, "{}: case {case}, n = {n}", m.key());
assert!(got.amps == want.amps, "{}: case {case}, n = {n}: the lanes differ", m.key());
assert!(run(m.run_lanes(&state, &gates)) == got, "{}: case {case}, n = {n}: per-gate and tiled differ", m.key());
if want.out_of_range.is_some() {
flagged += 1;
} else {
clean += 1;
}
}
assert!(flagged >= 100 && clean >= 100 && runs >= 1000, "{}: {flagged} flagged, {clean} clean, {runs} with a run of two gates or more", m.key());
report(&format!("webgpu tile kernel: key={} ok", m.key()));
// The four pinned certificates, under the tile implementation.
for (gs, random_pin, fabric_pin) in PINS {
for (corpus, cases, pin) in [(RANDOM_V1, 200, random_pin), (FABRIC_DEFAULT, 224, fabric_pin)] {
let c = blocking::certify(&tiled, gs, corpus).unwrap_or_else(|e| panic!("{} tiled {gs:?} {}: {e}", m.key(), corpus.name()));
assert_eq!((c.parity, c.cases), (Parity::BitExact, cases), "{} tiled {gs:?} {}", m.key(), corpus.name());
assert_eq!(hex(&c.corpus), pin, "{} tiled {gs:?} {}", m.key(), corpus.name());
assert_eq!((c.kernel.as_str(), c.implementation.as_str(), c.member.as_str()), (apply_kernel(gs), tiled.implementation(), m.id()));
assert_eq!(c.generator, corpus.generator(gs));
report(&format!(
"webgpu certified: key={} member={} kernel={} corpus={} implementation={} parity=bit-exact cases={} digest={}",
m.key(),
m.id(),
c.kernel,
corpus.name(),
c.implementation,
c.cases,
hex(&c.corpus)
));
}
}
// fabric-v1 (tiles of 1024, up to 128 of them) on forced geometries: one
// workgroup to a row, and three (a ragged last row), with rings of two
// records and the planes read back in pieces.
for max_x in [1, 3] {
let forced = tiled.with_max_x(max_x).with_ring_records(2).with_readback_chunk(12 * 1024 + 4);
for (gs, _, pin) in PINS {
let c = run(forced.certify(gs, FABRIC_DEFAULT));
assert_eq!((c.parity, hex(&c.corpus)), (Parity::BitExact, pin.to_string()), "{} tiled max_x {max_x} {gs:?}", m.key());
}
}
report(&format!("webgpu tile geometry: key={} ok", m.key()));
// The conformance vectors.
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance");
let mut total = 0;
for (dir, manifest) in [("vectors", "expected_hashes.json"), ("v2/vectors", "v2/expected_hashes.json")] {
let manifest: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(root.join(manifest)).expect("the manifest reads")).expect("the manifest parses");
let mut files: Vec<_> = std::fs::read_dir(root.join(dir)).expect("the vectors list").map(|e| e.expect("an entry").path()).filter(|p| p.extension().is_some_and(|x| x == "wqc")).collect();
files.sort();
assert!(!files.is_empty(), "no vectors in {dir}");
for path in files {
let name = path.file_name().unwrap().to_str().unwrap().to_string();
let (c, _) = from_wqc(&std::fs::read(&path).unwrap()).unwrap_or_else(|e| panic!("{dir}/{name}: {e}"));
let state = run(tiled.simulate(&c));
assert_eq!(Some(hex(&state.statevector_hash()).as_str()), manifest[&name]["statevector_hash"].as_str(), "{} tiled {dir}/{name}", m.key());
total += 1;
}
}
report(&format!("webgpu tile vectors: key={} {total} ok", m.key()));
// Repeated runs agree.
let c = random_circuit(18, 200, 0x7265_7065_6174, Gateset::V1);
let native = hex(&c.simulate().unwrap().statevector_hash());
let hashes: Vec<String> = (0..5).map(|_| hex(&blocking::simulate_from(&tiled, &c, 0).unwrap().statevector_hash())).collect();
assert!(hashes.iter().all(|h| *h == native), "{}: {hashes:?}, native {native}", m.key());
report(&format!("webgpu tile repeated: key={} 5 runs, one hash", m.key()));
// A start other than |0…0⟩, reduced modulo 2^n as the native simulator does.
for n in [12u8, 16] {
let c = random_circuit(n, 200, 0x7374_6172 + u64::from(n), Gateset::V2);
for start in [5usize, (1 << n) + 5] {
let native = c.simulate_from(start).unwrap().statevector_hash();
assert_eq!(hex(&blocking::simulate_from(&tiled, &c, start).unwrap().statevector_hash()), hex(&native), "{} tiled n = {n} start {start}", m.key());
}
}
// The first op whose result leaves 32 bits sits in a later submission
// than the first, and a later op leaves them again: the report names
// the first op's place in the op-log. The filler is circuit/1's P(19),
// which is not the identity, so the tile planner keeps every gate; on
// two qubits it touches only amplitudes that are zero.
let p19 = lanes::gate_words(&Gate { base: BaseGate::P, controls: vec![], target: 0, param: 19 }, Gateset::V1);
let z1 = lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 1, param: 0 }, Gateset::V2);
let state: [Lane; 4] = [[0, 0], [0, 0], [i32::MIN, 0], [0, 0]];
let gates: Vec<GateWords> = (0..5000).map(|k| if k == 4100 || k == 4500 { z1 } else { p19 }).collect();
let want = lanes::run_words(&state, &gates).expect("valid shapes");
assert_eq!(want.out_of_range, Some(4100));
for member in [tiled.clone(), tiled.with_ring_records(3)] {
assert_eq!(run(member.run_lanes(&state, &gates)), want, "{} tiled, first out of range at 4100, {member:?}", m.key());
}
// A run longer than one dispatch may hold: at 20 qubits a dispatch of
// SUBMIT_BUDGET pair-updates is 512 gates, and a run may hold 1024; the
// member splits it, and the state is the per-gate member's.
let mut s = 0x7370_6c69u64;
let diagonal = [BaseGate::Z, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
let mut long_run = Circuit::with_gateset(20, Gateset::V2);
for _ in 0..600 {
let base = diagonal[(next(&mut s) % 6) as usize];
let target = (next(&mut s) % 20) as u8;
let param = if base == BaseGate::P { 1 + (next(&mut s) % 32) as u16 } else { 0 };
long_run.ops.push(Gate { base, controls: vec![], target, param });
}
let planned = plan::compile_with(&long_run, 0, &Config::TILED).unwrap();
assert_eq!(planned.steps, [Step::Tile { first: 0, count: 600, size: 1024 }]);
assert!(600 > (wai_quantum::quantum_fabric_webgpu::SUBMIT_BUDGET >> 19));
let per_gate = blocking::simulate_from(m, &long_run, 0).unwrap().statevector_hash();
assert_eq!(hex(&blocking::simulate_from(&tiled, &long_run, 0).unwrap().statevector_hash()), hex(&per_gate), "{} a split run", m.key());
// Planned gates a member may not run are refused before anything
// runs, by both implementations: a `diag` that disagrees with the
// words, and the op index the tile kernel keeps for "no op".
let x0 = PlannedGate::new(lanes::gate_words(&Gate { base: BaseGate::X, controls: vec![], target: 0, param: 0 }, Gateset::V2), 0);
for g in [PlannedGate { diag: true, ..x0 }, PlannedGate { index: u32::MAX, ..x0 }] {
for (member, step) in [(m, Step::Gate { gate: 0 }), (&tiled, Step::Gate { gate: 0 }), (&tiled, Step::Tile { first: 0, count: 1, size: 4 })] {
let p = Program { n_qubits: 2, start: 0, gates: vec![g], steps: vec![step] };
assert_eq!(attempt(member.run_program(&p)).unwrap_err(), WebGpuError::Internal(4), "{} {g:?} {step:?}", m.key());
}
}
report(&format!("webgpu tile edges: key={} ok", m.key()));
// The widest states, as g6 runs them per gate: a run cap of 16 gates
// and a grid of (8, 8192) tiles at 26 qubits.
if std::env::var("WAI_QUANTUM_WEBGPU_WIDE").is_ok_and(|v| v == "1") && m.class() == FabricClass::Gpu {
let mut sizes = vec![24u8];
if m.max_qubits() > 24 {
sizes.push(m.max_qubits());
}
for n in sizes {
let qft = Circuit::qft(n);
let random = random_circuit(n, 200, 0x77_6964_65 + u64::from(n), Gateset::V2);
for c in [&qft, &random] {
let native = c.simulate().unwrap().statevector_hash();
let got = blocking::simulate_from(&tiled, c, 0).unwrap_or_else(|e| panic!("{} tiled n = {n}: {e}", m.key()));
assert_eq!(hex(&got.statevector_hash()), hex(&native), "{} tiled n = {n}", m.key());
}
report(&format!("webgpu tile wide: key={} n={n} matches native", m.key()));
}
}
// Timing, per-gate and tiled on the same circuits: printed, never
// asserted. The hashes must agree.
// On 10 qubits every gate fits one tile: runs of up to 1024 gates.
let long = random_circuit(10, 4000, 0x6c6f_6e67, Gateset::V2);
for (what, c) in [
("qft", Circuit::qft(16)),
("random", random_circuit(16, 400, 0x7469_6d65, Gateset::V2)),
("random", random_circuit(20, 400, 0x7469_6d65 + 1, Gateset::V2)),
("random", random_circuit(22, 200, 0x7469_6d65 + 2, Gateset::V2)),
("long", long),
] {
let steps = plan::compile_with(&c, 0, &Config::TILED).unwrap().steps.len();
let (per_gate_ms, a) = timed(3, || blocking::simulate_from(m, &c, 0).unwrap().statevector_hash());
let (tiled_ms, b) = timed(3, || blocking::simulate_from(&tiled, &c, 0).unwrap().statevector_hash());
assert_eq!(hex(&a), hex(&b), "{} {what} n = {}", m.key(), c.n_qubits);
report(&format!(
"webgpu timing: key={} {what} n={} gates={}: per-gate {per_gate_ms:.1} ms ({} dispatches), tiled {tiled_ms:.1} ms ({steps} dispatches)",
m.key(),
c.n_qubits,
c.ops.len(),
c.ops.len()
));
}
}
}
/// A meter for these tests (the crate ships none): a fixed reading, and how
/// often each end was called.
struct FixedMeter {
reading: Option<(u64, EnergyClass)>,
starts: u32,
stops: u32,
}
impl FixedMeter {
fn new(reading: Option<(u64, EnergyClass)>) -> FixedMeter {
FixedMeter { reading, starts: 0, stops: 0 }
}
}
impl RunMeter for FixedMeter {
fn start(&mut self) -> Result<(), String> {
self.starts += 1;
Ok(())
}
fn stop(&mut self) -> Result<Option<(u64, EnergyClass)>, String> {
self.stops += 1;
Ok(self.reading.clone())
}
}
fn counter() -> EnergyClass {
EnergyClass::OnChipCounter { uncertainty: Uncertainty { relative_ppm: 50_000, window_us: 1_000 }, filtering: CounterFiltering::Off }
}
#[test]
fn g13_metered_runs_and_the_fabric() {
let signer = SigningKey::from_bytes(&[7; 32]);
let local = detect_local();
for m in members() {
// A metered run: one window, and the reading only when its label is
// accepted.
let c = random_circuit(8, 60, 0x6d65_7465, Gateset::V2);
let native = c.simulate_from(3).unwrap();
let mut meter = FixedMeter::new(Some((1234, counter())));
let (state, joules, class) = blocking::simulate_metered(m, &c, 3, &mut meter).unwrap();
assert_eq!(state, native, "{}", m.key());
assert_eq!((joules, class, meter.starts, meter.stops), (1234, Some(counter()), 1, 1));
// Under the resolution floor: unmetered, the meter stopped once.
let mut below = FixedMeter::new(Some((1, counter())));
let (state, joules, class) = blocking::simulate_metered(m, &c, 3, &mut below).unwrap();
assert_eq!((state == native, joules, class, below.starts, below.stops), (true, 0, None, 1, 1));
// Nothing submitted, no window: refusals before the device, and the
// register of no qubits.
let mut idle = FixedMeter::new(Some((1234, counter())));
assert!(matches!(blocking::simulate_metered(m, &refused_circuits()[0], 0, &mut idle), Err(WebGpuError::Circuit(_))));
assert_eq!(blocking::simulate_metered(&m.with_max_qubits(3), &c, 0, &mut idle).unwrap_err(), WebGpuError::TooWide { n_qubits: 8, max: 3 });
let (empty, joules, class) = blocking::simulate_metered(m, &Circuit::new(0), 0, &mut idle).unwrap();
assert_eq!((empty, joules, class), (Circuit::new(0).simulate().unwrap(), 0, None));
assert_eq!((idle.starts, idle.stops), (0, 0));
// A metered calibration: the certificates member() earns, and a figure
// from every run's reading.
let mut meter = FixedMeter::new(Some((1000, counter())));
let metered = blocking::member_metered(m, RANDOM_V1, &mut meter).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!((meter.starts, meter.stops), (400, 400));
for ((entry, gs), (_, pin, _)) in metered.kernels.iter().zip(Gateset::ALL).zip(PINS) {
assert_eq!((entry.certificate.parity, hex(&entry.certificate.corpus)), (Parity::BitExact, pin.to_string()), "{} {gs:?}", m.key());
let mut expect = EnergyCalibration::new();
for c in RANDOM_V1.circuits(gs) {
expect.add(pair_updates(&c), 1000, Some(&counter()));
}
assert_eq!(entry.energy, expect.finish());
assert!(entry.energy.as_ref().is_some_and(|(nj, class)| *nj > 0 && class.label() == "OnChipCounter"));
}
// The fabric: the member first by synthetic figures (1 nJ per
// operation against the CPU reference's 10), holding three qubits.
let figured = |member: &Member, nj: u64| {
let mut member = member.clone();
member.kernels.iter_mut().for_each(|k| k.energy = Some((nj, counter())));
member
};
let gpu = figured(&metered, 1);
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add_metered(gpu.clone(), m.with_max_qubits(3), FixedMeter::new(Some((1234, counter()))));
// Wider than it holds: it refuses, and the CPU reference produces the
// result.
let wide = random_circuit(6, 40, 0x7769_6465, Gateset::V1);
let out = blocking::resolve(fabric.run(&wide, 0, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, wide.simulate().unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (local.id.as_str(), REFERENCE));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (0, None));
assert!(out.receipt.verify());
assert_eq!(out.refusals, [Refused { member_id: m.id().into(), implementation: m.implementation().into(), error: MemberError::TooWide { n_qubits: 6, max: 3 } }]);
// Within it: the member produces the result, metered, and the receipt
// binds its certificate.
let narrow = random_circuit(3, 40, 0x6e61_7272, Gateset::V2);
let out = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, narrow.simulate_from(5).unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (m.id(), m.implementation()));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (1234, Some(&counter())));
assert!(out.receipt.verify());
let entry = gpu.kernels.iter().find(|k| k.kernel == apply_kernel(Gateset::V2)).unwrap();
assert!(out.receipt.certificate_matches(&entry.certificate));
assert!(out.refusals.is_empty());
// The tile implementation on the same device: a metered run gives the
// native state inside one window, and on the fabric its receipt names
// the tile implementation and binds that implementation's certificate.
let tiled = run(m.tiled());
assert_ne!(tiled.implementation(), m.implementation());
let mut meter = FixedMeter::new(Some((1234, counter())));
let (state, joules, class) = blocking::simulate_metered(&tiled, &c, 3, &mut meter).unwrap();
assert_eq!(state, native, "{} tiled", m.key());
assert_eq!((joules, class, meter.starts, meter.stops), (1234, Some(counter()), 1, 1));
let tile_member = figured(&blocking::member(&tiled, RANDOM_V1).unwrap_or_else(|e| panic!("{}: {e}", m.key())), 1);
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add_metered(tile_member.clone(), tiled.clone(), FixedMeter::new(Some((1234, counter()))));
let out = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, narrow.simulate_from(5).unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (tiled.id(), tiled.implementation()));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (1234, Some(&counter())));
assert!(out.receipt.verify());
let entry = tile_member.kernels.iter().find(|k| k.kernel == apply_kernel(Gateset::V2)).unwrap();
assert!(out.receipt.certificate_matches(&entry.certificate));
// The tile implementation's entries with the per-gate runner: the
// runner does not run that implementation, which stops the run, and
// nothing is sealed.
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add(tile_member.clone(), m.clone());
let e = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_err();
assert!(matches!(&e.refusals[..], [Refused { error: MemberError::Other(_), .. }]), "{} {:?}", m.key(), e.refusals);
// A member that is not the runner's: refused before anything runs.
let mut stranger = gpu.clone();
stranger.id = "local/gpu-test-0123456789ab".into();
stranger.kernels.iter_mut().for_each(|k| k.certificate.member = stranger.id.clone());
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add(stranger.clone(), m.clone());
let e = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_err();
assert!(matches!(&e.refusals[..], [Refused { member_id, error: MemberError::Other(why), .. }] if *member_id == stranger.id && why.contains(m.id())), "{} {:?}", m.key(), e.refusals);
report(&format!("webgpu fabric: key={} ok", m.key()));
}
}