The propagation's sum of terms can now live on a device (quantum_spd::device). The device runs each step's arithmetic on every term at once: one multiply by sin, one by cos, one addition where a new term meets an old one, and sign changes, built without fused multiply-add. The host keeps everything that decides the term set. A rotation's new terms are found in the table, dead slots included, and the rest are appended in the order of the terms they came from by an order-preserving prefix sum. The host counts dead and live terms and compacts exactly when the CPU does. A truncation hands back the magnitudes it dropped in table order, and the bound and the value are added on the host in table order (expectation_on over an SpdDevice). Ground truth first: HostDevice runs that protocol on the CPU, one term after another, and is the CPU's propagation bit for bit, final table included, over 100 cases at one thread and many. The CUDA member (CudaSpd; kernels quantum_fabric::spd, implementation cuda-spd) is certified on SPD_DEFAULT against the CPU. The corpus is 40 cases on one to four words, with term and weight caps, layered circuits whose dropped strings come back and are revived in place, and one exact cancellation. Each case's output is every value, bound, threshold reached and count, and the final table slot for slot, so a device that keeps the numbers but not the term set or its order is caught: a mutant that appended a revived string again survived a corpus of numbers alone. All six mutants are killed. On the laptop GPU: certified, and 24 wider random cases up to two million terms and the 127-qubit kicked-Ising circuit are the CPU's results bit for bit. A pauli job asks with fabric (accelerator, or a member's id or key): its sum of terms runs on that member, with the CPU's answers byte for byte, and the result's fabric names the member and its certificate (kernel wai.quantum.spd/propagate). cpu names the CPU, and a split is refused (J013). auto keeps the method on the CPU, since no member is measured faster for it. Its members are discovered apart from the state vector's, and wai-quantum fabric lists them (pauli, optional, in the tools schema). The CUDA leg certifies the propagation and kills its mutants; the placement legs run a pauli job on each member, or see accelerator refused; the container legs name the protocol's tests. Docs, help, both MCP servers' descriptions, the guide and the changelog say so; agent docs re-blessed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
104 lines
5.4 KiB
Rust
104 lines
5.4 KiB
Rust
//! Sparse Pauli dynamics on the CUDA devices of the machine that runs this:
|
|
//! the CPU's result bit for bit (value, bound, threshold reached, peak, final
|
|
//! terms, term updates), or the certificate says not.
|
|
//!
|
|
//! The machine declares its devices in `WAI_QUANTUM_CUDA_EXPECT` (comma-
|
|
//! separated keys, or `none`), as for `cuda_member`; `WAI_QUANTUM_CUDA_REPORT`
|
|
//! names a file the established lines are appended to.
|
|
#![cfg(not(target_arch = "wasm32"))]
|
|
|
|
use std::io::Write;
|
|
use wai_quantum::quantum_fabric::spd::{MUTANTS, mutate};
|
|
use wai_quantum::quantum_fabric::{Certification, Parity};
|
|
use wai_quantum::quantum_fabric_cuda::{CudaMember, discover};
|
|
use wai_quantum::quantum_spd::device::{KERNEL_SPD, SPD_DEFAULT, SPD_DEFAULT_PIN, SpdCorpus, expectation_on};
|
|
use wai_quantum::quantum_spd::{SpdConfig, expectation_capped, heavy_hex_127, kicked_ising};
|
|
|
|
fn report(line: &str) {
|
|
println!("{line}");
|
|
if let Some(path) = std::env::var_os("WAI_QUANTUM_CUDA_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");
|
|
}
|
|
}
|
|
|
|
fn members() -> Vec<CudaMember> {
|
|
let d = discover();
|
|
let mut found: Vec<String> = d.found.iter().map(|m| m.key().to_string()).collect();
|
|
found.sort();
|
|
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
|
|
let expect = std::env::var("WAI_QUANTUM_CUDA_EXPECT").unwrap_or_else(|_| panic!("WAI_QUANTUM_CUDA_EXPECT is not set; found {found_list}"));
|
|
assert_eq!(found_list, expect, "the devices found are not the declared ones");
|
|
d.found
|
|
}
|
|
|
|
fn hex(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
/// The CPU certifying itself on `corpus`: the digest a bit-exact device
|
|
/// reproduces.
|
|
fn cpu_digest(corpus: SpdCorpus) -> String {
|
|
let mut cert = Certification::new();
|
|
for case in corpus.cases() {
|
|
let r = case.reference().unwrap();
|
|
cert.add(&case.bytes(), &r, &r);
|
|
}
|
|
hex(&cert.finish(KERNEL_SPD, "cpu", "cpu", &corpus.generator()).corpus)
|
|
}
|
|
|
|
#[test]
|
|
fn d0_the_propagation_is_the_cpus_bit_for_bit() {
|
|
let cpu = cpu_digest(SPD_DEFAULT);
|
|
assert_eq!(cpu, SPD_DEFAULT_PIN, "this machine's CPU does not reproduce the pin");
|
|
for m in members() {
|
|
let spd = m.spd().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
|
let c = spd.certify(SPD_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
|
assert_eq!(c.parity, Parity::BitExact, "{}", m.key());
|
|
assert_eq!(hex(&c.corpus), cpu, "{}: the digest is not the CPU's", m.key());
|
|
report(&format!("cuda spd certified: key={} member={} kernel={} implementation={} parity=bit-exact cases={} digest={}", m.key(), m.id(), c.kernel, c.implementation, c.cases, hex(&c.corpus)));
|
|
}
|
|
}
|
|
|
|
/// Beyond the corpus: wider random cases, and the 127-qubit kicked-Ising
|
|
/// circuit on the heavy-hex map, whose sum grows past a hundred thousand
|
|
/// terms; each the CPU's result bit for bit.
|
|
#[test]
|
|
fn d1_large_sums_match_the_cpu() {
|
|
for m in members() {
|
|
let spd = m.spd().unwrap();
|
|
let mut largest = 0;
|
|
for case in (SpdCorpus { seed: 77, cases: 24, max_qubits: 20, depth: 160 }).cases() {
|
|
let want = expectation_capped(&case.circuit, &case.observable, &case.ones, &case.cfg, 8, case.max_terms).unwrap();
|
|
let got = expectation_on(&mut spd.run(), &case.circuit, &case.observable, &case.ones, &case.cfg, case.max_terms).unwrap();
|
|
assert_eq!((got.0.value.to_bits(), got.0.truncation_bound.to_bits(), got.0.peak_terms, got.0.final_terms, got.0.term_updates, got.1.to_bits()), (want.0.value.to_bits(), want.0.truncation_bound.to_bits(), want.0.peak_terms, want.0.final_terms, want.0.term_updates, want.1.to_bits()), "{}", m.key());
|
|
largest = largest.max(want.0.peak_terms);
|
|
}
|
|
let c = kicked_ising(127, &heavy_hex_127(), 0.6, 5);
|
|
let obs = vec![(1.0, vec![(62u32, wai_quantum::quantum_spd::Pauli::Z)])];
|
|
let cfg = SpdConfig { threshold: 1e-4, max_weight: None };
|
|
let want = expectation_capped(&c, &obs, &[], &cfg, 8, usize::MAX).unwrap();
|
|
let got = expectation_on(&mut spd.run(), &c, &obs, &[], &cfg, usize::MAX).unwrap();
|
|
assert_eq!((got.0.value.to_bits(), got.0.truncation_bound.to_bits(), got.0.peak_terms, got.0.final_terms, got.0.term_updates), (want.0.value.to_bits(), want.0.truncation_bound.to_bits(), want.0.peak_terms, want.0.final_terms, want.0.term_updates), "{}: kicked Ising", m.key());
|
|
report(&format!("cuda spd large: key={} 24 random cases (peak {largest} terms) and kicked Ising on 127 qubits (peak {} terms) match the CPU bit for bit", m.key(), want.0.peak_terms));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn d2_mutants_are_caught() {
|
|
let small = SPD_DEFAULT;
|
|
let cpu = cpu_digest(small);
|
|
for m in members() {
|
|
let mut killed = 0;
|
|
for mutant in &MUTANTS {
|
|
let spd = m.spd_with_source(&mutate(mutant)).unwrap_or_else(|e| panic!("mutant {}: {e}", mutant.name));
|
|
match spd.certify(small) {
|
|
Ok(c) => assert!(c.parity != Parity::BitExact || hex(&c.corpus) != cpu, "{}: mutant {} survived", m.key(), mutant.name),
|
|
// A mutant that makes the device fail is caught too.
|
|
Err(_) => {}
|
|
}
|
|
killed += 1;
|
|
}
|
|
report(&format!("cuda spd mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
|
|
}
|
|
}
|