//! 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 = 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 { 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 = 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(f: impl Future>) -> Result { blocking::resolve(f).expect("a native call resolves in one poll") } /// A member call that must succeed. fn run(f: impl Future>) -> 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 { let gate = |base, controls: Vec, 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 = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect(); let gates: Vec = (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 = (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 = (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(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 = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect(); let gates: Vec = (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 = 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 = (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 = (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, 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())); } }