open-standards/wai/wai-quantum/tests/webgpu_member.rs
Transaction Science 2cd6ab4672 wai-quantum WebGPU member: chunked work under test, a failed wait is typed
Review findings on the member, none of which changed a result:

- Ring rewrites and multi-piece readback were never exercised below the
  wide sizes: every corpus circuit fit one submission and one readback
  piece. Hidden knobs `with_ring_records` and `with_readback_chunk` (like
  `with_max_x`) let G4 certify random-v1 through a ring of three records
  with state planes read back twelve bytes at a time, and fabric-v1
  through a ring of two with pieces of 12 KiB and four bytes; both give
  the pinned digests. G6 adds long four-qubit circuits in each gateset,
  more gates than one submission holds, at the default ring and a ring of
  three, against native; and a run whose first out-of-range op sits in a
  later submission than the first, with a later one after it, reported
  at its op-log index.
- wgpu's native device poll reports a failed wait other than a timeout,
  such as device loss, by panicking. The poll now runs under
  catch_unwind: the panic becomes WebGpuError::Device and the device is
  treated as lost from then on. Documented, including that a
  panic = "abort" build still aborts. A unit test holds the mapping.
- POLL_TIMEOUT bounded the whole run's GPU time, since the first wait was
  on the readback queued behind every gate submission. Natively each
  submission, a chunk of gates sized by the work budget, the probe's
  dispatch or a readback copy, is now waited on before the next, so the
  timeout bounds one chunk. The documentation and changelog say so.

Still 12 device tests; the member script's checks are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 23:40:08 -04:00

543 lines
26 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 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::Program;
use wai_quantum::quantum_fabric::wgsl::{self, Detector, MUTANTS};
use wai_quantum::quantum_fabric::{Corpus, FABRIC_DEFAULT, FabricClass, FabricReceipt, Parity, Presence, REFERENCE, apply_kernel, detect_local, 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()));
}
}
#[test]
fn g8_mutants_are_killed() {
let edges = probe_pairs(0, 0);
assert_eq!(edges.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");
let caught = match mutant.detector {
Detector::Probe => {
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 {}: {differing} of 484 edge pairs differ", mutant.name, m.key());
differing >= 1
}
Detector::Certificate => Gateset::ALL.into_iter().all(|gs| match attempt(mm.certify(gs, RANDOM_V1)) {
Err(e) => {
println!("webgpu mutant {} on {}: {gs:?} certification refused: {e}", mutant.name, m.key());
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)),
};
assert!(caught, "{}: 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()));
}
}
#[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");
}