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]>
543 lines
26 KiB
Rust
543 lines
26 KiB
Rust
//! 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");
|
||
}
|