// The fabric's WebGPU member in a browser, held to the CPU reference's pins. // // The native members are checked by scripts/wai-quantum-webgpu-member.sh. This // checks the browser's: the same WGSL kernel compiled by the browser's own WGSL // compiler, run in a worker by wai-web/demo/quantum_webgpu.html with the bundle // built by // // cd wai/wai-web // cargo build --locked --release --target wasm32-unknown-unknown --features quantum_webgpu // wasm-bindgen --target web --out-dir demo/pkg-webgpu --out-name wai_quantum_webgpu \ // target/wasm32-unknown-unknown/release/wai_web.wasm // // It serves wai-web/demo and quantum-conformance on 127.0.0.1, opens the page in // Playwright's headless browser and requires: // // - an adapter opened as a member, its arithmetic probed in the page, running the // implementation fabric/expected.json names; // - the four certificates fabric/expected.json lists (both gatesets, random-v1 and // fabric-v1), each bit-exact, on that member, for that implementation, over the // listed number of cases, with the pinned corpus digest; // - every vector of vectors/ and v2/vectors/ run on the member to its manifest's // statevector hash; // - the same adapter running the tile implementation fabric/expected.json names // (tile runs: the browser's WGSL compiler holds its barrier to the uniformity // rules, which the native translator does not check), with the same four // certificates and every vector; // - every file of reject/ refused with its listed class (`WQC parse (): ...`); // - a circuit one qubit wider than the member holds refused: as too-wide, or as // too-many-qubits where the member holds the reference's maximum; and, with the // same adapter offering only the WebGPU default buffer limits, a member holding // 25 qubits that refuses 26 as too-wide. // // On success it prints // // browser webgpu: member= class= certified bit-exact 4 of 4; tiles certified bit-exact 4 of 4; vectors of ; tile vectors of ; rejects of // // With no adapter it prints `browser webgpu: no adapter ()`, which is a // failure when WAI_QUANTUM_WEBGPU_REQUIRE=1 and not otherwise. // // [WAI_QUANTUM_WEBGPU_REQUIRE=1] [PLAYWRIGHT_HOST=] \ // node wai/quantum-conformance/browser_webgpu.mjs // // Headless, a page is offered no hardware adapter unless the browser draws through // the platform's GPU interface: on macOS that is `--use-angle=metal`, kept below. // WAI_QUANTUM_WEBGPU_BROWSER_ARGS (space-separated) replaces the launch arguments. import http from "node:http"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const DEMO = path.resolve(HERE, "../wai-web/demo"); const PLAYWRIGHT_HOST = process.env.PLAYWRIGHT_HOST || "/Users/dcharlot/data-share/vibe-coding/joulesperbit/"; const REQUIRE = process.env.WAI_QUANTUM_WEBGPU_REQUIRE === "1"; const PORT = Number(process.env.PORT || 8795); // The certificates take the longest: a few hundred circuits per gateset, each run // on the device and read back. const TIMEOUT_MS = Number(process.env.WAI_QUANTUM_WEBGPU_TIMEOUT_MS || 30 * 60 * 1000); const ARGS = process.env.WAI_QUANTUM_WEBGPU_BROWSER_ARGS !== undefined ? process.env.WAI_QUANTUM_WEBGPU_BROWSER_ARGS.split(/\s+/).filter(Boolean) : process.platform === "darwin" ? ["--use-angle=metal"] : []; const { chromium } = createRequire(PLAYWRIGHT_HOST)("playwright"); const readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8")); // What the page must report, read from the committed corpus. const EXPECTED = readJson(path.join(HERE, "fabric", "expected.json")); const VECTORS = {}; for (const prefix of ["", "v2/"]) { for (const [file, e] of Object.entries(readJson(path.join(HERE, prefix, "expected_hashes.json")))) { VECTORS[prefix + file] = e.statevector_hash; } } const REJECTS = Object.fromEntries( Object.entries(readJson(path.join(HERE, "reject", "reject.json"))).map(([file, kind]) => ["reject/" + file, kind])); const MAX_QUBITS = 26; // wai::quantum::MAX_QUBITS const DEFAULT_LIMITS_QUBITS = 25; // two planes of 2^25 u32 in a 128 MiB binding // Two roots, at the paths they have under wai/, so the page's default corpus // location (../../quantum-conformance/) holds here as it does under any static // server run from wai/. const ROOTS = { "/wai-web/demo/": DEMO, "/quantum-conformance/": HERE }; const MIME = { ".js": "text/javascript", ".mjs": "text/javascript", ".wasm": "application/wasm", ".html": "text/html", ".json": "application/json" }; const server = http.createServer((req, res) => { const url = decodeURIComponent(req.url.split("?")[0]); const prefix = Object.keys(ROOTS).find((p) => url.startsWith(p)); const root = prefix && ROOTS[prefix]; const f = root && path.resolve(root, "." + url.slice(prefix.length - 1)); if (!f || !f.startsWith(root + path.sep) || !fs.existsSync(f) || !fs.statSync(f).isFile()) { res.writeHead(404); return res.end("not found"); } res.writeHead(200, { "content-type": MIME[path.extname(f)] || "application/octet-stream" }); fs.createReadStream(f).pipe(res); }); let browser; let fail = 0; const report = (label, problem) => { if (problem) { fail++; console.log(`FAIL ${label}: ${problem}`); } else { console.log(`PASS ${label}`); } }; process.on("unhandledRejection", (e) => { console.error("UNHANDLED:", e); process.exitCode = 1; }); try { if (!fs.existsSync(path.join(DEMO, "pkg-webgpu", "wai_quantum_webgpu.js"))) { console.error(`no pkg-webgpu/wai_quantum_webgpu.js under ${DEMO}; build it as this file's header says`); process.exit(2); } await new Promise((r) => server.listen(PORT, "127.0.0.1", r)); browser = await chromium.launch({ args: ARGS }); const page = await browser.newPage(); const errs = []; page.on("console", (m) => { if (m.type() === "error") errs.push(m.text()); }); page.on("pageerror", (e) => errs.push(String(e))); // domcontentloaded, never networkidle: the page's work runs in a worker after // load, and networkidle stalls on some hosts. await page.goto(`http://127.0.0.1:${PORT}/wai-web/demo/quantum_webgpu.html`, { waitUntil: "domcontentloaded", timeout: 30000 }); await page.waitForFunction(() => window.quantumWebGpu && window.quantumWebGpu.done === true, null, { timeout: TIMEOUT_MS, polling: 1000 }); const messages = await page.evaluate(() => window.quantumWebGpu.messages); const of = (type, limits) => messages.filter((m) => m.type === type && (limits === undefined || m.limits === limits)); for (const m of messages.filter((m) => m.type === "error" || m.type === "refused")) { report(`the page (${m.type})`, m.error || m.why); } const none = of("no-adapter")[0]; const member = of("member", "offered")[0]; if (!member) { const why = none ? none.why : "the page reported no member and no reason"; console.log(`browser webgpu: no adapter (${why})`); if (REQUIRE || !none || fail) process.exitCode = 1; } else { // The member. console.log(`member ${member.id}: ${member.detail}`); report(`member ${member.id} (class ${member.class})`, !/^local\/(gpu|swgpu)-webgpu-[0-9a-f]{12}$/.test(member.id) ? "not a WebGPU member id" : (member.class === "cpu") !== member.id.startsWith("local/swgpu-") ? "its class and id disagree" : null); report("the probe passed in the page", member.detail.includes("probe passed in this process") ? null : `detail: ${member.detail}`); report(`implementation ${member.implementation}`, member.implementation === EXPECTED.implementation ? null : `fabric/expected.json pins ${EXPECTED.implementation}`); // The tile member: the same adapter, the pinned tile implementation. const tiled = of("tiled")[0]; report(`the tile member runs ${EXPECTED.tile_implementation}`, !tiled ? "not reported" : tiled.error ? `refused: ${tiled.error}` : tiled.id !== member.id ? `member ${tiled.id}` : tiled.implementation !== EXPECTED.tile_implementation ? `implementation ${tiled.implementation}` : null); // The four certificates, per implementation. const certify = (tile, implementation) => { let certified = 0; for (const want of EXPECTED.certificates) { const label = `${tile ? "tile " : ""}${want.kernel} ${want.corpus}`; const got = of("certificate").find((m) => m.label === label && !!m.tile === tile); const c = got && got.got; const generator = ` seed=${want.seed} cases=${want.cases}` + (want.corpus === "fabric-v1" ? ` wide=${want.wide}` : ""); const problem = !got ? "not reported" : got.error ? `refused: ${got.error}` : c.parity !== "bit-exact" ? `parity ${c.parity}` : c.kernel !== want.kernel ? `kernel ${c.kernel}` : c.implementation !== implementation ? `implementation ${c.implementation}` : c.member !== member.id ? `issued to ${c.member}` : c.cases !== want.cases_compared ? `${c.cases} cases, not ${want.cases_compared}` : !c.generator.endsWith(generator) ? `generator ${c.generator}` : c.corpus_hex !== want.corpus_hex ? `corpus ${c.corpus_hex}, pinned ${want.corpus_hex}` : null; report(`certificate ${label} bit-exact with the pinned digest`, problem); if (!problem) certified++; } return certified; }; const certified = certify(false, member.implementation); const tileCertified = certify(true, EXPECTED.tile_implementation); // Every vector, per implementation. const run = (tile) => { let vectors = 0; for (const [label, want] of Object.entries(VECTORS)) { const got = of("vector").find((m) => m.label === label && !!m.tile === tile); const problem = !got ? "not reported" : got.error ? `refused: ${got.error}` : got.got !== want ? `browser=${got.got} corpus=${want}` : null; report(`${tile ? "tile " : ""}${label}`, problem); if (!problem) vectors++; } return vectors; }; const vectors = run(false); const tileVectors = run(true); // Every reject file. let rejects = 0; for (const [label, kind] of Object.entries(REJECTS)) { const got = of("reject").find((m) => m.label === label); const m = got && got.error && /^WQC parse \(([a-z-]+)\): /.exec(got.error); const problem = !got ? "not reported" : !got.error ? "accepted" : !m || m[1] !== kind ? `refused as: ${got.error}` : null; report(`${label} refused as ${kind}`, problem); if (!problem) rejects++; } // One qubit too wide, on the limits offered and on the WebGPU defaults. const wide = of("wide", "offered")[0]; const wantClass = member.max_qubits + 1 > MAX_QUBITS ? "too-many-qubits" : "too-wide"; report(`${member.max_qubits + 1} qubits on the member refused as ${wantClass}`, !wide ? "not reported" : !wide.error ? "accepted" : !wide.error.startsWith(`simulate (${wantClass}): `) ? `refused as: ${wide.error}` : null); const capped = of("member", "webgpu-defaults")[0]; const cappedWide = of("wide", "webgpu-defaults")[0]; if (capped) console.log(`member ${capped.id} (the same adapter on the WebGPU default limits): ${capped.detail}`); report(`on the WebGPU default limits the member holds ${DEFAULT_LIMITS_QUBITS} qubits and refuses ${DEFAULT_LIMITS_QUBITS + 1} as too-wide`, !capped ? "no member on the default limits" : capped.max_qubits !== DEFAULT_LIMITS_QUBITS ? `it holds ${capped.max_qubits}` : !cappedWide ? "not reported" : !cappedWide.error ? "accepted" : !cappedWide.error.startsWith(`simulate (too-wide): ${DEFAULT_LIMITS_QUBITS + 1} qubits; this member holds at most ${DEFAULT_LIMITS_QUBITS}`) ? `refused as: ${cappedWide.error}` : null); report("console errors", errs.length ? `${errs.length}\n ${errs.join("\n ")}` : null); const n = Object.keys(VECTORS).length; const r = Object.keys(REJECTS).length; if (fail === 0) { const k = EXPECTED.certificates.length; console.log(`browser webgpu: member=${member.id} class=${member.class} certified bit-exact ${certified} of ${k}; tiles certified bit-exact ${tileCertified} of ${k}; vectors ${vectors} of ${n}; tile vectors ${tileVectors} of ${n}; rejects ${rejects} of ${r}`); } else { console.log(`browser webgpu: ${fail} checks failed`); } process.exitCode = fail ? 1 : 0; } } catch (e) { console.error("HARNESS ERROR:", e); process.exitCode = 1; } finally { if (browser) await browser.close(); server.close(); }