plan (PLAN_REVISION 2): Config { tile_bits, run_max, skip_identity } with
PER_GATE (what compile makes) and TILED (tiles of up to 1024 amplitudes, runs
of up to 1024 gates, capped at 2^30 / 2^n, 16 at 26 qubits); Step::Tile
{ first, count, size }; compile_with, schedule, execute_from,
PlannedGate::{new, is_identity}. A tile run is a maximal run of consecutive
gates that act inside a tile: diagonal gates on any target, others with their
target's bit below the tile size. Only exact identities are skipped (circuit/1's
P(19..=32) is 2^30 - 1 and stays). The CPU executor mirrors the tile kernel's
index arithmetic tile by tile; lanes::exact_row is shared with apply_words.
Tests: every configuration (10 of them) equals the native simulator over
random-v1 (300) and fabric-v1 and certifies with the four lanes32 pins; long
random circuits and full-range random words tile exactly as they run gate by
gate; fabric-v1 covers every tile path; malformed programs are refused;
a_rounded_product_is_never_negated runs under every configuration.
wgsl: TILE_SHADER = lanes32.wgsl followed by lanes32_tile.wgsl (entry point
apply_tile, workgroup 256, one workgroup per tile, the tile size, gate range
and grid in a 32-byte uniform run record, gates in a 48-byte read-only storage
record), tile_implementation_id() = wgsl-lanes32-tile/de6fb388454a0144 (pinned),
tile records, tile_grid (x at most 8), TILE_MUTANTS T1 (barrier between gates
removed, detector Race over race_witness), T2 (the report's minimum dropped,
Range) and T3 (tbit < size -> <=, Malformed over malformed_tile_run).
Deviation from the plan's kernel text: no workgroup memory. naga 30.0.1
decorates every array type with ArrayStride and struct members with Offset,
and spirv-val (SPIRV-Tools 2026.4) rejects explicit layouts on Workgroup
variables (VUID-StandaloneSpirv-None-10684); per the plan the kernel was
restructured instead: each workgroup updates its own tile in place in the state
planes, with storageBarrier() between gates. The uniformity argument is
unchanged (one barrier, at the top level of the loop over the run's gates).
The min of the report is taken once per gate so that T2's text occurs once; a
tile outside the state reports bit 2; a record outside the gate buffer reads
as malformed.
Checker: SHADER and TILE_SHADER each under their own rules (entry points and
workgroup sizes, bindings, no workgroup memory, bool/u32 only, literal shifts,
atomics, barrier kind and placement at the top level of the body or of a
top-level for loop with no break, continue or return), translation to MSL,
SPIR-V and HLSL, mutants; lanes32_tile.spv for spirv-val; --tile-shader.
Member: WebGpuMember::tiled() compiles TILE_SHADER, probes it, plans with
TILED and reports the tile implementation; per-gate stays the default and the
fallback. Runs interleave gate and tile dispatches per submission (a second
uniform ring for run records, a storage buffer for the runs' gates), within
SUBMIT_BUDGET. Device tests: g8 kills T1-T3 on the tile member; new g12 runs
2,000 full-range cases tiled vs per-gate vs the CPU, the four pinned
certificates under the tile implementation, forced geometries, the conformance
vectors, five repeated runs, and prints per-gate vs tiled timing. The member
script requires 13 tests and both implementations' certificates and mutants.
Browser: QuantumWebGpu.tiled(); the worker certifies and runs the vectors on
the tile member too; browser_webgpu.mjs requires "tiles certified bit-exact 4
of 4" and fabric/expected.json pins tile_implementation.
CI: checks validate lanes32_tile.spv and package lanes32_tile.wgsl; the
browser job checks the tile pin and greps the tile certificates; the wasip2 job
requires the new tests by name, and its timeout is 90 minutes (the planner test
runs several minutes alone in a debug wasm build).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
242 lines
13 KiB
JavaScript
242 lines
13 KiB
JavaScript
// 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 (<class>): ...`);
|
|
// - 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=<id> class=<c> certified bit-exact 4 of 4; tiles certified bit-exact 4 of 4; vectors <n> of <n>; tile vectors <n> of <n>; rejects <m> of <m>
|
|
//
|
|
// With no adapter it prints `browser webgpu: no adapter (<why>)`, which is a
|
|
// failure when WAI_QUANTUM_WEBGPU_REQUIRE=1 and not otherwise.
|
|
//
|
|
// [WAI_QUANTUM_WEBGPU_REQUIRE=1] [PLAYWRIGHT_HOST=<dir with node_modules/playwright>] \
|
|
// 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();
|
|
}
|