From 9094d6f73d13ea3fcf0b799d0ec3e5af145d3cd2 Mon Sep 17 00:00:00 2001
From: Transaction Science
Date: Sun, 11 Oct 2026 05:50:30 -0400
Subject: [PATCH 1/4] wai-quantum WebGPU member in a browser: wai-web bundle,
page, check, CI
wai-web gains the feature `quantum_webgpu` (wai/quantum_fabric,
wai/quantum_fabric_webgpu and wasm-bindgen-futures 0.4.77), which compiles
src/quantum_webgpu.rs on wasm32: quantum_webgpu_discover() opens the page's
adapter as a fabric member and probes its arithmetic; QuantumWebGpu reports the
member and returns promises for certify (random-v1 or fabric-v1, as certificate
JSON) and statevector_hash_hex (rejections read ` (): `).
The default build is unchanged: its bundle has the same size, and differs only
in the five panic-location line numbers of src/lib.rs that the new module
declaration shifts. wai-web/Cargo.lock only adds entries (web-sys resolves to
0.3.104 beside js-sys 0.3.104 and wasm-bindgen 0.2.127); indexmap's hashbrown
entry is relabelled with its version.
demo/quantum_webgpu.html runs demo/quantum_webgpu_worker.js, which certifies
both gatesets on random-v1 and fabric-v1, runs every quantum vector, offers
every reject file and a circuit one qubit wider than the member holds; a second
worker repeats the width check with the adapter offering the WebGPU default
buffer limits, where the member holds 25 qubits and refuses 26 as too-wide.
The bundle builds into demo/pkg-webgpu (ignored, as demo/pkg is).
quantum-conformance/browser_webgpu.mjs drives the page headless and holds every
result to the committed corpus and to fabric/expected.json (the four pinned
digests and the implementation id). On macOS a headless page is offered a
hardware adapter only with --use-angle=metal, which the script passes.
CI: the checks job builds the bundle; a new browser job on macos checks that
fabric/expected.json's pins are the crate's, builds the bundle with the
wasm-bindgen CLI 0.2.127, installs Playwright 1.60.0 and its headless browser
into the job's temp directory, and requires a GPU member certified bit-exact
4 of 4.
Co-Authored-By: Claude Opus 5.5
---
.github/workflows/wai-quantum-webgpu.yml | 79 ++
wai/.gitignore | 2 +
wai/CHANGELOG.md | 9 +
wai/quantum-conformance/README.md | 17 +
wai/quantum-conformance/browser_webgpu.mjs | 219 ++++++
wai/quantum-conformance/fabric/expected.json | 45 ++
wai/wai-web/Cargo.lock | 762 ++++++++++++++++++-
wai/wai-web/Cargo.toml | 11 +
wai/wai-web/demo/README.md | 30 +
wai/wai-web/demo/quantum_webgpu.html | 120 +++
wai/wai-web/demo/quantum_webgpu_worker.js | 192 +++++
wai/wai-web/src/lib.rs | 7 +
wai/wai-web/src/quantum_webgpu.rs | 187 +++++
13 files changed, 1679 insertions(+), 1 deletion(-)
create mode 100644 wai/quantum-conformance/browser_webgpu.mjs
create mode 100644 wai/quantum-conformance/fabric/expected.json
create mode 100644 wai/wai-web/demo/quantum_webgpu.html
create mode 100644 wai/wai-web/demo/quantum_webgpu_worker.js
create mode 100644 wai/wai-web/src/quantum_webgpu.rs
diff --git a/.github/workflows/wai-quantum-webgpu.yml b/.github/workflows/wai-quantum-webgpu.yml
index bffec17bd..ba2724949 100644
--- a/.github/workflows/wai-quantum-webgpu.yml
+++ b/.github/workflows/wai-quantum-webgpu.yml
@@ -14,6 +14,12 @@ name: wai-quantum WebGPU member
# --no-default-features: its default feature, `codecs`, links system codec
# libraries that neither GPU host has, and the corpus needs none of them.
#
+# The browser job runs the same kernel through a browser's WebGPU, compiled by the
+# browser's own WGSL compiler: wai-web's `quantum_webgpu` bundle in a headless page
+# (wai/quantum-conformance/browser_webgpu.mjs), which must certify a GPU member with
+# the four pinned digests, reproduce every vector and refuse every reject file with
+# its class. It is not a required status until it has been green three runs in a row.
+#
# There is no windows job while the windows runner is offline: a job queued on an
# offline runner never settles. Add it back from the comment at the end when the
# runner has survived a reboot.
@@ -35,6 +41,9 @@ on:
- 'wai/wai-rs/Cargo.lock'
- 'wai/wai-rs/src/lib.rs'
- 'rust-toolchain.toml'
+ # The checks and browser jobs build wai-web with its feature, --locked.
+ - 'wai/wai-web/Cargo.toml'
+ - 'wai/wai-web/Cargo.lock'
- 'wai/wai-web/src/quantum_webgpu.rs'
- 'wai/wai-web/demo/quantum_webgpu*'
- 'scripts/wai-quantum-webgpu-member.sh'
@@ -50,6 +59,9 @@ on:
- 'wai/wai-rs/Cargo.lock'
- 'wai/wai-rs/src/lib.rs'
- 'rust-toolchain.toml'
+ # The checks and browser jobs build wai-web with its feature, --locked.
+ - 'wai/wai-web/Cargo.toml'
+ - 'wai/wai-web/Cargo.lock'
- 'wai/wai-web/src/quantum_webgpu.rs'
- 'wai/wai-web/demo/quantum_webgpu*'
- 'scripts/wai-quantum-webgpu-member.sh'
@@ -91,6 +103,9 @@ jobs:
- name: The member builds for a browser
working-directory: wai/wai-quantum
run: cargo build --locked --lib --target wasm32-unknown-unknown --no-default-features --features quantum_fabric_webgpu
+ - name: The browser bundle builds with the member
+ working-directory: wai/wai-web
+ run: cargo build --locked --release --target wasm32-unknown-unknown --features quantum_webgpu
- name: On wasm32-wasip2 the feature is inert
shell: bash
working-directory: wai/wai-quantum
@@ -210,6 +225,70 @@ jobs:
working-directory: src/wai/wai-rs
run: CARGO_TARGET_DIR="$HOME/hw-target/wai-rs" nice -n 10 cargo run --locked --no-default-features --features quantum_fabric_webgpu --bin wai_quantum_vectors -- verify ../quantum-conformance --webgpu
+ browser:
+ name: member · browser WebGPU (macos)
+ runs-on: macos
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v5
+ # launchd's PATH omits cargo on the macOS host runner.
+ - name: Put cargo on PATH
+ shell: bash
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+ # The page's pins are the ones the crate's tests hold, so that editing the page's
+ # file alone cannot make a wrong member pass.
+ - name: The page's pins are the crate's
+ shell: bash
+ run: |
+ set -euo pipefail
+ f=wai/quantum-conformance/fabric/expected.json
+ digests="$(python3 -I -c 'import json, sys; print("\n".join(c["corpus_hex"] for c in json.load(open(sys.argv[1]))["certificates"]))' "$f")"
+ [ "$(printf '%s\n' "$digests" | grep -cE '^[0-9a-f]{64}$')" -eq 4 ] || { echo "::error::$f does not list four digests"; exit 1; }
+ for d in $digests; do
+ grep -q "\"$d\"" wai/wai-quantum/src/quantum_fabric.rs || { echo "::error::$d is not pinned in wai-quantum"; exit 1; }
+ done
+ implementation="$(python3 -I -c 'import json, sys; print(json.load(open(sys.argv[1]))["implementation"])' "$f")"
+ grep -q "PINNED_IMPLEMENTATION: &str = \"$implementation\";" wai/wai-quantum/src/quantum_fabric/wgsl.rs \
+ || { echo "::error::$implementation is not the pinned implementation id"; exit 1; }
+ # Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the crate
+ # must match exactly.
+ - name: wasm-bindgen CLI 0.2.127
+ shell: bash
+ run: |
+ set -euo pipefail
+ rustup target add wasm32-unknown-unknown
+ cargo install --locked wasm-bindgen-cli --version 0.2.127
+ - name: The member's bundle
+ shell: bash
+ working-directory: wai/wai-web
+ run: |
+ set -euo pipefail
+ nice -n 10 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
+ # Playwright and its headless browser go into the job's temporary directory, not
+ # the host's caches. `chromium` is Playwright's name for its engine.
+ - name: Playwright 1.60.0 and its headless browser
+ shell: bash
+ run: |
+ set -euo pipefail
+ node --version
+ pw="$RUNNER_TEMP/playwright"
+ mkdir -p "$pw"
+ npm install --prefix "$pw" --no-save --no-audit --no-fund playwright@1.60.0
+ PLAYWRIGHT_BROWSERS_PATH="$pw/browsers" "$pw/node_modules/.bin/playwright" install --only-shell chromium
+ echo "PLAYWRIGHT_HOST=$pw/" >> "$GITHUB_ENV"
+ echo "PLAYWRIGHT_BROWSERS_PATH=$pw/browsers" >> "$GITHUB_ENV"
+ - name: The member in a browser
+ shell: bash
+ run: |
+ set -euo pipefail
+ WAI_QUANTUM_WEBGPU_REQUIRE=1 nice -n 10 node wai/quantum-conformance/browser_webgpu.mjs 2>&1 \
+ | tee "$RUNNER_TEMP/browser-webgpu.log"
+ grep -qE '^browser webgpu: member=local/gpu-webgpu-[0-9a-f]{12} class=gpu certified bit-exact 4 of 4; ' \
+ "$RUNNER_TEMP/browser-webgpu.log" \
+ || { echo "::error::no GPU member certified bit-exact 4 of 4 in the browser"; exit 1; }
+
# Windows, when its runner is back (DX12, translator naga+dxbc, and Vulkan, as separate members):
# member-windows:
# runs-on: windows
diff --git a/wai/.gitignore b/wai/.gitignore
index 225b95c31..e8b50522c 100644
--- a/wai/.gitignore
+++ b/wai/.gitignore
@@ -51,6 +51,8 @@ wai-web/demo/scpkg/
# wasm-pack build outputs (rebuilt by wasm-pack / demo/verify_int_synth.sh).
wai-web/demo/pkg/
wai-web/demo/pkg-node/
+# The WebGPU member's bundle (demo/quantum_webgpu.html; built with --features quantum_webgpu).
+wai-web/demo/pkg-webgpu/
# int_hyper conditional-codec demo artifacts (PTQ model + bitstream, large —
# regenerated by tools/wai_sota_int_synth_export.py into demo/int_hyper/).
wai-web/demo/int_hyper/
diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md
index 53b257caf..f15dfec86 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -79,6 +79,15 @@ Additive. The default features, `full`, and every existing pin and digest are un
if the adapters found differ, if any test did not run, if a declared adapter lacks a
bit-exact certificate for each pinned digest, or if a shader mutant survives. The
`wasm32-wasip2` job requires the fabric's new unit tests by name.
+- **The member in a browser.** wai-web's feature `quantum_webgpu` binds the member for JS
+ (`quantum_webgpu_discover`, `QuantumWebGpu`: `certify`, `statevector_hash_hex`) in a
+ bundle of its own (`--out-name wai_quantum_webgpu`); the default build and its bundle
+ are unchanged. `demo/quantum_webgpu.html` runs the member in a worker, its kernel
+ compiled by the browser's own WGSL compiler. `quantum-conformance/browser_webgpu.mjs`
+ drives the page headless and requires the four certificates bit-exact with the pinned
+ digests (`quantum-conformance/fabric/expected.json`), every vector's statevector hash,
+ every reject file refused with its class, and a circuit wider than the member holds
+ refused. The `checks` job builds the bundle, and a `browser` job runs the page on macOS.
### wai-web on older engines: the reach builds
diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md
index d40d872c6..d1d08dac4 100644
--- a/wai/quantum-conformance/README.md
+++ b/wai/quantum-conformance/README.md
@@ -57,6 +57,23 @@ matching manifest. It is run by hand, before the website deploys a bundle. Note
the IDE's `statevector_hash_hex()` is empty until `run()` is called — the circuit is
built eagerly, the state is not.
+## Browser, on the fabric's WebGPU member
+
+```sh
+WAI_QUANTUM_WEBGPU_REQUIRE=1 node wai/quantum-conformance/browser_webgpu.mjs
+```
+
+Serves `wai-web/demo` and this directory on 127.0.0.1 and opens
+`quantum_webgpu.html` in Playwright's headless browser, with the `quantum_webgpu`
+bundle built into `wai-web/demo/pkg-webgpu` (`wai-web/demo/README.md`). The gate
+kernel runs on the page's WebGPU adapter, compiled by the browser's own WGSL
+compiler. The page must certify the kernel bit-exact for both gatesets on random-v1
+and fabric-v1 with the digests `fabric/expected.json` pins (the CPU reference's), run
+every vector of both trees to its statevector hash, refuse every reject file with its
+listed class, and refuse a circuit wider than the member holds. Without
+`WAI_QUANTUM_WEBGPU_REQUIRE=1`, a page offered no adapter is reported and passes. CI
+runs it on macOS (`wai-quantum-webgpu.yml`, job `browser`).
+
## Referee
```sh
diff --git a/wai/quantum-conformance/browser_webgpu.mjs b/wai/quantum-conformance/browser_webgpu.mjs
new file mode 100644
index 000000000..b2555f3b8
--- /dev/null
+++ b/wai/quantum-conformance/browser_webgpu.mjs
@@ -0,0 +1,219 @@
+// 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;
+// - 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; 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 four certificates.
+ let certified = 0;
+ for (const want of EXPECTED.certificates) {
+ const label = `${want.kernel} ${want.corpus}`;
+ const got = of("certificate").find((m) => m.label === label);
+ 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 !== member.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++;
+ }
+
+ // Every vector.
+ let vectors = 0;
+ for (const [label, want] of Object.entries(VECTORS)) {
+ const got = of("vector").find((m) => m.label === label);
+ const problem = !got ? "not reported" : got.error ? `refused: ${got.error}` : got.got !== want ? `browser=${got.got} corpus=${want}` : null;
+ report(label, problem);
+ if (!problem) vectors++;
+ }
+
+ // 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) {
+ console.log(`browser webgpu: member=${member.id} class=${member.class} certified bit-exact ${certified} of ${EXPECTED.certificates.length}; vectors ${vectors} 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();
+}
diff --git a/wai/quantum-conformance/fabric/expected.json b/wai/quantum-conformance/fabric/expected.json
new file mode 100644
index 000000000..8f863b2b1
--- /dev/null
+++ b/wai/quantum-conformance/fabric/expected.json
@@ -0,0 +1,45 @@
+{
+ "implementation": "wgsl-lanes32/dc8e19f61e0e87ab",
+ "certificates": [
+ {
+ "gateset": 1,
+ "kernel": "wai.quantum.circuit/apply",
+ "corpus": "random-v1",
+ "seed": 7,
+ "cases": 200,
+ "wide": 0,
+ "cases_compared": 200,
+ "corpus_hex": "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36"
+ },
+ {
+ "gateset": 2,
+ "kernel": "wai.quantum.circuit2/apply",
+ "corpus": "random-v1",
+ "seed": 7,
+ "cases": 200,
+ "wide": 0,
+ "cases_compared": 200,
+ "corpus_hex": "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2"
+ },
+ {
+ "gateset": 1,
+ "kernel": "wai.quantum.circuit/apply",
+ "corpus": "fabric-v1",
+ "seed": 7,
+ "cases": 200,
+ "wide": 24,
+ "cases_compared": 224,
+ "corpus_hex": "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7"
+ },
+ {
+ "gateset": 2,
+ "kernel": "wai.quantum.circuit2/apply",
+ "corpus": "fabric-v1",
+ "seed": 7,
+ "cases": 200,
+ "wide": 24,
+ "cases_compared": 224,
+ "corpus_hex": "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809"
+ }
+ ]
+}
diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock
index 11586aacc..9b9edf0e5 100644
--- a/wai/wai-web/Cargo.lock
+++ b/wai/wai-web/Cargo.lock
@@ -2,12 +2,36 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+[[package]]
+name = "ash"
+version = "0.38.0+1.3.281"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
+dependencies = [
+ "libloading",
+]
+
[[package]]
name = "async-trait"
version = "0.1.92"
@@ -31,6 +55,21 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+[[package]]
+name = "bit-set"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+
[[package]]
name = "bitflags"
version = "2.13.1"
@@ -59,12 +98,41 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
[[package]]
name = "cast"
version = "0.3.0"
@@ -105,6 +173,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "codespan-reporting"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
+dependencies = [
+ "serde",
+ "termcolor",
+ "unicode-width",
+]
+
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -135,6 +220,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -192,6 +283,25 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags",
+ "objc2",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
[[package]]
name = "ed25519"
version = "2.2.3"
@@ -250,6 +360,12 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "futures-core"
version = "0.3.34"
@@ -306,11 +422,51 @@ dependencies = [
"r-efi",
]
+[[package]]
+name = "gpu-allocator"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
+dependencies = [
+ "ash",
+ "hashbrown 0.16.1",
+ "log",
+ "presser",
+ "thiserror",
+ "windows",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "foldhash",
+]
[[package]]
name = "heck"
@@ -325,7 +481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
- "hashbrown",
+ "hashbrown 0.17.1",
]
[[package]]
@@ -351,6 +507,16 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
[[package]]
name = "libm"
version = "0.2.16"
@@ -363,6 +529,21 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
[[package]]
name = "log"
version = "0.4.34"
@@ -385,6 +566,44 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "naga"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "libm",
+ "log",
+ "naga-types",
+ "num-traits",
+ "once_cell",
+ "rustc-hash",
+ "spirv",
+ "thiserror",
+ "unicode-ident",
+]
+
+[[package]]
+name = "naga-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
+dependencies = [
+ "hashbrown 0.17.1",
+ "indexmap",
+ "rustc-hash",
+ "thiserror",
+]
+
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -423,6 +642,92 @@ dependencies = [
"libm",
]
+[[package]]
+name = "objc2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -435,6 +740,38 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+[[package]]
+name = "ordered-float"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -457,6 +794,27 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "presser"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -466,6 +824,12 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+
[[package]]
name = "quote"
version = "1.0.47"
@@ -490,6 +854,51 @@ dependencies = [
"getrandom 0.2.17",
]
+[[package]]
+name = "range-alloc"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "raw-window-metal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
+dependencies = [
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "renderdoc-sys"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -527,6 +936,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
[[package]]
name = "semver"
version = "1.0.28"
@@ -629,6 +1044,21 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+
+[[package]]
+name = "spirv"
+version = "0.4.0+sdk-1.4.341.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "spki"
version = "0.7.3"
@@ -639,6 +1069,12 @@ dependencies = [
"der",
]
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
[[package]]
name = "subtle"
version = "2.6.1"
@@ -680,6 +1116,35 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
[[package]]
name = "toml"
version = "0.9.12+spec-1.1.0"
@@ -731,6 +1196,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -760,6 +1231,7 @@ dependencies = [
"blake3",
"ed25519-dalek",
"serde_json",
+ "wgpu",
]
[[package]]
@@ -774,6 +1246,7 @@ dependencies = [
"serde_json",
"wai",
"wasm-bindgen",
+ "wasm-bindgen-futures",
"wasm-bindgen-test",
]
@@ -889,6 +1362,169 @@ version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ff1c1b360982e93b6d8ea9c04836f71dba0817a16f91e229cf3a51bdd9d987"
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wgpu"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.17.1",
+ "js-sys",
+ "log",
+ "naga",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bit-vec",
+ "bitflags",
+ "bytemuck",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "log",
+ "naga",
+ "naga-types",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash",
+ "smallvec",
+ "thiserror",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-windows-linux-android",
+ "wgpu-hal",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set",
+ "bitflags",
+ "block2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "gpu-allocator",
+ "hashbrown 0.17.1",
+ "libc",
+ "libloading",
+ "log",
+ "naga",
+ "naga-types",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-metal",
+ "objc2-quartz-core",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "raw-window-metal",
+ "renderdoc-sys",
+ "smallvec",
+ "static_assertions",
+ "thiserror",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "wgpu-naga-bridge"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
+dependencies = [
+ "naga",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "30.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "naga-types",
+ "raw-window-handle",
+ "static_assertions",
+ "web-sys",
+]
+
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -898,12 +1534,107 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -913,6 +1644,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "winnow"
version = "0.7.15"
@@ -925,6 +1665,26 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+[[package]]
+name = "zerocopy"
+version = "0.8.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "zeroize"
version = "1.9.0"
diff --git a/wai/wai-web/Cargo.toml b/wai/wai-web/Cargo.toml
index 36759563b..30b4bdc43 100644
--- a/wai/wai-web/Cargo.toml
+++ b/wai/wai-web/Cargo.toml
@@ -22,6 +22,17 @@ js-sys = "0.3"
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs)
blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest
+# Awaits the WebGPU member's futures behind the promises of src/quantum_webgpu.rs;
+# only the `quantum_webgpu` feature compiles it.
+wasm-bindgen-futures = { version = "0.4.77", optional = true }
+
+[features]
+# The fabric's WebGPU member in a page (src/quantum_webgpu.rs): the gate kernel's
+# WGSL compiled by the browser's own WebGPU, certified in the page against the CPU
+# reference's pinned corpus digests. Built as its own bundle with its own
+# --out-name (demo/quantum_webgpu.html); off by default, so the main bundle is
+# unchanged.
+quantum_webgpu = ["wai/quantum_fabric", "wai/quantum_fabric_webgpu", "dep:wasm-bindgen-futures"]
[dev-dependencies]
# Pinned to the wasm-bindgen release Cargo.lock holds, so the test runner and the
diff --git a/wai/wai-web/demo/README.md b/wai/wai-web/demo/README.md
index 14d4d3736..0f630e5fb 100644
--- a/wai/wai-web/demo/README.md
+++ b/wai/wai-web/demo/README.md
@@ -115,3 +115,33 @@ give the wasm32 build's verdicts on every session it sealed:
# from wai-web/ (claim_verify.py needs the `cryptography` package)
bash demo/verify_measure.sh
```
+
+## `quantum_webgpu.html` — the fabric's WebGPU member in a page
+
+`src/quantum_webgpu.rs` (feature `quantum_webgpu`, its own bundle) binds
+`wai::quantum_fabric_webgpu` for JS: `quantum_webgpu_discover()` opens the adapter the
+page is offered as a fabric member and probes its arithmetic against the CPU's, and the
+member it returns certifies the gate kernel against the CPU reference on a corpus
+(`certify`) and runs a `WQC` container to its statevector hash (`statevector_hash_hex`).
+The kernel is the same WGSL text the native member runs, compiled by the page's own
+WebGPU. A bit-exact member reproduces the CPU reference's pinned corpus digests,
+`quantum-conformance/fabric/expected.json`.
+
+The page runs `quantum_webgpu_worker.js` in a module worker: the four pinned
+certificates (both gatesets, random-v1 and fabric-v1), every vector of the quantum
+corpus, every reject file (each refused with its listed class), and a circuit one qubit
+wider than the member holds. A second worker repeats the last check with the adapter
+offering only the WebGPU default buffer limits, where the member holds 25 qubits and
+refuses 26 as too wide.
+
+```
+# from wai-web/: the bundle has its own --out-name, so its imports never meet the main bundle's
+cargo build --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
+# serve wai/: the page reads ../../quantum-conformance/
+cd .. && python3 -m http.server 8741 # → http://localhost:8741/wai-web/demo/quantum_webgpu.html
+```
+
+`quantum-conformance/browser_webgpu.mjs` drives the page headless and checks every
+result against the committed corpus.
diff --git a/wai/wai-web/demo/quantum_webgpu.html b/wai/wai-web/demo/quantum_webgpu.html
new file mode 100644
index 000000000..0d08ec5d3
--- /dev/null
+++ b/wai/wai-web/demo/quantum_webgpu.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+WAI — the fabric's WebGPU member
+
+
+
+ WAI wai.quantum.fabric — the WebGPU member in this page
+
+ The gate kernel of wai.quantum.circuit and wai.quantum.circuit2, written in
+ WGSL with unsigned 32-bit arithmetic only, is compiled here by this page's own WebGPU and run in a
+ worker. The adapter's arithmetic is probed against the CPU's first; the kernel is then certified
+ against the CPU reference on two corpora per gateset. A bit-exact member reproduces the reference's
+ pinned corpus digests (quantum-conformance/fabric/expected.json); every vector of the
+ quantum corpus must reproduce its statevector hash, and every reject file must be refused with its class.
+
+ starting
+
+
+
+
diff --git a/wai/wai-web/demo/quantum_webgpu_worker.js b/wai/wai-web/demo/quantum_webgpu_worker.js
new file mode 100644
index 000000000..6f41a7c66
--- /dev/null
+++ b/wai/wai-web/demo/quantum_webgpu_worker.js
@@ -0,0 +1,192 @@
+// The fabric's WebGPU member, run in a module worker (quantum_webgpu.html starts
+// it). It opens the adapter this worker is offered as a member, probing its
+// arithmetic against the CPU's, then:
+//
+// - certifies the gate kernel of both gatesets on random-v1 and fabric-v1, the
+// four certificates quantum-conformance/fabric/expected.json pins;
+// - runs every vector of quantum-conformance/vectors/ and v2/vectors/ on the
+// member to its statevector hash;
+// - offers it every file of quantum-conformance/reject/, each of which must be
+// refused with its listed class;
+// - offers it a circuit one qubit wider than it holds: refused as too-wide, or as
+// too-many-qubits where it already holds the reference's maximum.
+//
+// Each result is posted to the page as it arrives; the last message is
+// {type: "done"}. Query parameters:
+//
+// corpus= the quantum-conformance directory
+// (default ../../quantum-conformance/, as served from wai/)
+// limits=webgpu-defaults the adapter offers only the WebGPU default buffer limits
+// (128 MiB per storage binding, 256 MiB per buffer), as many
+// pages are offered; the worker then runs the too-wide check
+// alone. An adapter that holds the reference's maximum can
+// never be offered a valid circuit too wide for it; this
+// shows the refusal on the same device.
+
+import init, { quantum_webgpu_discover } from "./pkg-webgpu/wai_quantum_webgpu.js";
+
+const params = new URL(self.location.href).searchParams;
+const CORPUS = new URL(params.get("corpus") || "../../quantum-conformance/", self.location.href);
+const LIMITS = params.get("limits");
+
+// The widest state the reference simulates (wai::quantum::MAX_QUBITS).
+const MAX_QUBITS = 26;
+// The WebGPU default limits, in bytes.
+const DEFAULT_STORAGE_BINDING = 134217728;
+const DEFAULT_BUFFER = 268435456;
+
+const post = (m) => self.postMessage(m);
+const message = (e) => String((e && e.message) || e);
+
+async function fetchBytes(url) {
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
+ return new Uint8Array(await r.arrayBuffer());
+}
+
+async function fetchJson(url) {
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
+ return r.json();
+}
+
+// The same container with its contract's n_qubits set to `n`: a valid circuit
+// whose ops touch only the qubits they did, now `n` qubits wide. Sections keep
+// their order; their offsets are recomputed.
+function widen(wqc, n) {
+ const view = new DataView(wqc.buffer, wqc.byteOffset, wqc.byteLength);
+ const count = view.getUint16(4, true);
+ const blob = 6 + 9 * count;
+ const sections = [];
+ for (let k = 0; k < count; k++) {
+ const at = 6 + 9 * k;
+ const off = view.getUint32(at + 1, true);
+ const len = view.getUint32(at + 5, true);
+ sections.push({ kind: view.getUint8(at), bytes: wqc.subarray(blob + off, blob + off + len) });
+ }
+ const contract = sections.find((s) => s.kind === 1);
+ if (!contract) throw new Error("the container has no contract section");
+ const text = new TextDecoder().decode(contract.bytes);
+ const wider = text.replace(/"n_qubits":\d+/, `"n_qubits":${n}`);
+ if (wider === text) throw new Error("the contract names no n_qubits");
+ contract.bytes = new TextEncoder().encode(wider);
+ const out = new Uint8Array(blob + sections.reduce((t, s) => t + s.bytes.length, 0));
+ const w = new DataView(out.buffer);
+ out.set(wqc.subarray(0, 6));
+ let off = 0;
+ sections.forEach((s, k) => {
+ const at = 6 + 9 * k;
+ w.setUint8(at, s.kind);
+ w.setUint32(at + 1, off, true);
+ w.setUint32(at + 5, s.bytes.length, true);
+ out.set(s.bytes, blob + off);
+ off += s.bytes.length;
+ });
+ return out;
+}
+
+// Every adapter this worker is then offered reports the WebGPU default buffer
+// limits; the member requests what the adapter reports, so its device is granted
+// those.
+function offerDefaultLimits() {
+ const gpu = navigator.gpu;
+ const request = gpu.requestAdapter.bind(gpu);
+ gpu.requestAdapter = async (options) => {
+ const adapter = await request(options);
+ if (adapter) {
+ Object.defineProperty(adapter.limits, "maxStorageBufferBindingSize", { value: DEFAULT_STORAGE_BINDING });
+ Object.defineProperty(adapter.limits, "maxBufferSize", { value: DEFAULT_BUFFER });
+ }
+ return adapter;
+ };
+}
+
+// qft5 of circuit2, widened to one qubit more than the member holds.
+async function tooWide(member, limits) {
+ const n = member.max_qubits() + 1;
+ const bytes = widen(await fetchBytes(new URL("v2/vectors/qft5.wqc", CORPUS)), n);
+ const want = n > MAX_QUBITS ? "too-many-qubits" : "too-wide";
+ try {
+ const hash = await member.statevector_hash_hex(bytes);
+ post({ type: "wide", limits, n, max_qubits: member.max_qubits(), want, accepted: hash });
+ } catch (e) {
+ post({ type: "wide", limits, n, max_qubits: member.max_qubits(), want, error: message(e) });
+ }
+}
+
+async function run() {
+ if (!navigator.gpu) {
+ post({ type: "no-adapter", why: "navigator.gpu is absent in this worker" });
+ return;
+ }
+ if (LIMITS === "webgpu-defaults") offerDefaultLimits();
+ else if (LIMITS) throw new Error(`limits=${LIMITS}: the one value is webgpu-defaults`);
+ await init();
+
+ let member;
+ try {
+ member = await quantum_webgpu_discover();
+ } catch (e) {
+ const why = message(e);
+ post(why.startsWith("no adapter: ") ? { type: "no-adapter", why: why.slice("no adapter: ".length) } : { type: "refused", why });
+ return;
+ }
+ post({
+ type: "member",
+ limits: LIMITS || "offered",
+ id: member.member_id(),
+ key: member.key(),
+ class: member.class(),
+ detail: member.detail(),
+ max_qubits: member.max_qubits(),
+ implementation: member.implementation(),
+ });
+ if (LIMITS) {
+ await tooWide(member, LIMITS);
+ return;
+ }
+
+ // The four certificates.
+ const expected = await fetchJson(new URL("fabric/expected.json", CORPUS));
+ for (const c of expected.certificates) {
+ const label = `${c.kernel} ${c.corpus}`;
+ try {
+ const got = JSON.parse(await member.certify(c.gateset, c.corpus, BigInt(c.seed), c.cases, c.wide));
+ post({ type: "certificate", label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, got });
+ } catch (e) {
+ post({ type: "certificate", label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, error: message(e) });
+ }
+ }
+
+ // Every vector of both trees.
+ for (const prefix of ["", "v2/"]) {
+ const manifest = await fetchJson(new URL(`${prefix}expected_hashes.json`, CORPUS));
+ for (const [file, entry] of Object.entries(manifest)) {
+ const label = prefix + file;
+ try {
+ const bytes = await fetchBytes(new URL(`${prefix}vectors/${file}`, CORPUS));
+ post({ type: "vector", label, want: entry.statevector_hash, got: await member.statevector_hash_hex(bytes) });
+ } catch (e) {
+ post({ type: "vector", label, want: entry.statevector_hash, error: message(e) });
+ }
+ }
+ }
+
+ // Every reject file.
+ const listing = await fetchJson(new URL("reject/reject.json", CORPUS));
+ for (const [file, kind] of Object.entries(listing)) {
+ const label = "reject/" + file;
+ try {
+ const bytes = await fetchBytes(new URL(`reject/${file}`, CORPUS));
+ post({ type: "reject", label, want: kind, accepted: await member.statevector_hash_hex(bytes) });
+ } catch (e) {
+ post({ type: "reject", label, want: kind, error: message(e) });
+ }
+ }
+
+ await tooWide(member, "offered");
+}
+
+run()
+ .catch((e) => post({ type: "error", error: message(e) }))
+ .finally(() => post({ type: "done" }));
diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs
index af884ba2f..01b6ffffe 100644
--- a/wai/wai-web/src/lib.rs
+++ b/wai/wai-web/src/lib.rs
@@ -153,6 +153,13 @@ pub mod quantum_phasor;
// a total-energy roll-up, and show a swapped stage break the bill.
pub mod quantum_qbom;
+// The fabric's WebGPU member in a page (wai.quantum.fabric): the browser's adapter
+// opened and probed, the gate kernel certified on it against the CPU reference's
+// pinned corpus digests, and WQC containers run to their statevector hash. Only in
+// the separate `quantum_webgpu` bundle; the main bundle does not carry it.
+#[cfg(all(feature = "quantum_webgpu", target_arch = "wasm32"))]
+mod quantum_webgpu;
+
// Staged delivery (extensions/staged-delivery.md): wai::staged::StageSink, the sink a
// native player runs, bound for JS — bases added, refinement layers offered, holds
// expired and bases removed in any order, every unit's stage, producer, tier, output
diff --git a/wai/wai-web/src/quantum_webgpu.rs b/wai/wai-web/src/quantum_webgpu.rs
new file mode 100644
index 000000000..60fc2571d
--- /dev/null
+++ b/wai/wai-web/src/quantum_webgpu.rs
@@ -0,0 +1,187 @@
+//! The fabric's WebGPU member in a page (`wai::quantum_fabric_webgpu`), bound
+//! for JS in the separate `quantum_webgpu` bundle.
+//!
+//! [`quantum_webgpu_discover`] opens the adapter the page is offered as a
+//! member and probes its arithmetic against the CPU's in this page. The member
+//! then certifies the gate kernel against the CPU reference on a corpus
+//! ([`QuantumWebGpu::certify`]) and runs `WQC` containers to their statevector
+//! hash ([`QuantumWebGpu::statevector_hash_hex`]).
+//!
+//! The kernel is the same WGSL text the native member runs, compiled here by
+//! the browser's own WGSL compiler, so a bit-exact certificate from a page is
+//! evidence from an independent translator. The corpus digest covers inputs
+//! and outputs only, so a bit-exact member reproduces the CPU reference's
+//! pinned digests. Certificates are earned in the page that runs the work.
+//!
+//! Every method that uses the device returns a promise over a clone of the
+//! member's handle, so no borrow of the object is held across an await.
+
+use std::rc::Rc;
+
+use wai::quantum::{from_wqc, Gateset, QuantumError};
+use wai::quantum_fabric::{Corpus, Parity};
+use wai::quantum_fabric_webgpu::{discover, WebGpuError, WebGpuMember};
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_futures::future_to_promise;
+
+fn hex(b: &[u8]) -> String {
+ b.iter().map(|x| format!("{x:02x}")).collect()
+}
+
+/// The class word of a refusal: the reference's own class for a circuit it
+/// refuses (`malformed`, `invalid`, `contract`, `too-many-qubits`, as
+/// `reject.json` lists them), else the member's reason.
+fn class(e: &WebGpuError) -> &'static str {
+ match e {
+ WebGpuError::Circuit(q) => q.kind(),
+ WebGpuError::TooWide { .. } => "too-wide",
+ WebGpuError::OutOfRange { .. } => "out-of-range",
+ WebGpuError::NoAdapter(_) => "no-adapter",
+ WebGpuError::Unsuitable { .. } => "unsuitable",
+ WebGpuError::Device(_) => "device",
+ WebGpuError::Memory(_) => "memory",
+ WebGpuError::Timeout => "timeout",
+ WebGpuError::Pending => "pending",
+ WebGpuError::Internal(_) => "internal",
+ WebGpuError::Probe { .. } => "probe",
+ _ => "member",
+ }
+}
+
+/// A promise's rejection: an `Error` whose message is
+/// ` (): `, the form the quantum sink throws.
+fn refusal(stage: &str, class: &str, reason: &dyn std::fmt::Display) -> JsValue {
+ JsError::new(&format!("{stage} ({class}): {reason}")).into()
+}
+
+fn member_refusal(stage: &str, e: &WebGpuError) -> JsValue {
+ refusal(stage, class(e), e)
+}
+
+fn parse_refusal(e: &QuantumError) -> JsValue {
+ refusal("WQC parse", e.kind(), e)
+}
+
+fn parity_label(p: &Parity) -> &'static str {
+ match p {
+ Parity::BitExact => "bit-exact",
+ Parity::Within { .. } => "within",
+ Parity::Statistical { .. } => "statistical",
+ Parity::Uncertified => "uncertified",
+ }
+}
+
+/// The browser's adapter as a fabric member, its arithmetic probed in this page.
+#[wasm_bindgen]
+pub struct QuantumWebGpu {
+ member: Rc,
+}
+
+/// The browser's adapter as a fabric member, opened and probed in this page.
+/// Rejects with `no adapter: ` when the page is offered none, or with the
+/// adapter's refusal (`adapter () refused: `).
+#[wasm_bindgen]
+pub async fn quantum_webgpu_discover() -> Result {
+ let d = discover().await;
+ if let Some(member) = d.found.into_iter().next() {
+ return Ok(QuantumWebGpu { member: Rc::new(member) });
+ }
+ match d.refused.first() {
+ Some(r) => Err(JsError::new(&format!("adapter {} ({:?}) refused: {}", r.key, r.adapter, r.error))),
+ None => Err(JsError::new("no adapter: the page's WebGPU offered none")),
+ }
+}
+
+#[wasm_bindgen]
+impl QuantumWebGpu {
+ /// The member id: `local/gpu-webgpu-`, or `local/swgpu-webgpu-`
+ /// for a software adapter.
+ pub fn member_id(&self) -> String {
+ self.member.id().to_string()
+ }
+
+ /// The key a runner declares for it.
+ pub fn key(&self) -> String {
+ self.member.key().to_string()
+ }
+
+ /// `"gpu"`, or `"cpu"` for a software adapter.
+ pub fn class(&self) -> String {
+ self.member.class().label().to_string()
+ }
+
+ /// What discovery found: adapter, translator, granted limits. Not signed. A
+ /// page's WebGPU may report no adapter description; the member id then rests
+ /// on the rest of the identity, and this says so.
+ pub fn detail(&self) -> String {
+ let detail = self.member.detail();
+ if self.member.identity().name.is_empty() {
+ format!("{detail}; adapter identity withheld")
+ } else {
+ detail.to_string()
+ }
+ }
+
+ /// The widest circuit this member's buffers hold.
+ pub fn max_qubits(&self) -> u8 {
+ self.member.max_qubits()
+ }
+
+ /// The kernel implementation the member runs (`wgsl-lanes32/<16 hex>`).
+ pub fn implementation(&self) -> String {
+ self.member.implementation().to_string()
+ }
+
+ /// Certify the gate kernel of `gateset` (1 for `wai.quantum.circuit`, 2 for
+ /// `wai.quantum.circuit2`) on this member over a corpus: `"random-v1"`
+ /// (`seed`, `cases`; `wide` must be 0) or `"fabric-v1"` (`seed`, `cases`,
+ /// `wide`). `seed` is a `BigInt`. Resolves to the certificate as JSON:
+ /// `{kernel, implementation, member, generator, parity, cases, corpus_hex,
+ /// certificate_hash_hex}`; rejects with `certify (): `.
+ pub fn certify(&self, gateset: u8, corpus: String, seed: u64, cases: u32, wide: u32) -> js_sys::Promise {
+ let member = Rc::clone(&self.member);
+ future_to_promise(async move {
+ let gs = match gateset {
+ 1 => Gateset::V1,
+ 2 => Gateset::V2,
+ g => return Err(refusal("certify", "argument", &format!("gateset {g}: 1 is wai.quantum.circuit, 2 is wai.quantum.circuit2"))),
+ };
+ let corpus = match corpus.as_str() {
+ "random-v1" if wide == 0 => Corpus::Random { seed, cases: cases as usize },
+ "random-v1" => return Err(refusal("certify", "argument", &"random-v1 has no wide circuits; pass wide = 0")),
+ "fabric-v1" => Corpus::Fabric { seed, cases: cases as usize, wide: wide as usize },
+ other => return Err(refusal("certify", "argument", &format!("corpus {other:?}: random-v1 or fabric-v1"))),
+ };
+ let cert = member.certify(gs, corpus).await.map_err(|e| member_refusal("certify", &e))?;
+ let json = serde_json::json!({
+ "kernel": cert.kernel,
+ "implementation": cert.implementation,
+ "member": cert.member,
+ "generator": cert.generator,
+ "parity": parity_label(&cert.parity),
+ "cases": cert.cases,
+ "corpus_hex": hex(&cert.corpus),
+ "certificate_hash_hex": hex(&cert.hash()),
+ });
+ Ok(JsValue::from_str(&json.to_string()))
+ })
+ }
+
+ /// Parse a `WQC1` or `WQC2` container and run it on the member. Resolves to
+ /// the statevector hash in hex, the hash the native simulator gives; rejects
+ /// with ` (): `: `WQC parse` with the class
+ /// `reject.json` lists for a container the reference reader refuses, or
+ /// `simulate` with the reference's class for a circuit it refuses
+ /// (`too-many-qubits`), or the member's (`too-wide` for more qubits than
+ /// its buffers hold, `out-of-range` where the reference's result leaves 32
+ /// bits, `device`, `memory`, ...).
+ pub fn statevector_hash_hex(&self, wqc: Vec) -> js_sys::Promise {
+ let member = Rc::clone(&self.member);
+ future_to_promise(async move {
+ let (circuit, _measure) = from_wqc(&wqc).map_err(|e| parse_refusal(&e))?;
+ drop(wqc);
+ let state = member.simulate(&circuit).await.map_err(|e| member_refusal("simulate", &e))?;
+ Ok(JsValue::from_str(&hex(&state.statevector_hash())))
+ })
+ }
+}
--
2.54.0 (Apple Git-157)
From a998381232dbbe863897c8f296062de08e2f6dd2 Mon Sep 17 00:00:00 2001
From: Transaction Science
Date: Sun, 11 Oct 2026 10:12:29 -0400
Subject: [PATCH 2/4] wai-quantum WebGPU member: tile runs, a second
implementation certified on its own
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
---
.github/workflows/wai-conformance.yml | 26 +-
.github/workflows/wai-quantum-webgpu.yml | 16 +-
scripts/wai-quantum-webgpu-member.sh | 24 +-
wai/CHANGELOG.md | 21 +
wai/quantum-conformance/README.md | 8 +-
wai/quantum-conformance/browser_webgpu.mjs | 83 ++-
wai/quantum-conformance/fabric/expected.json | 1 +
wai/tools/quantum-wgsl-check/src/main.rs | 352 ++++++++---
wai/wai-quantum/README.md | 2 +-
wai/wai-quantum/src/quantum_fabric.rs | 50 +-
.../src/quantum_fabric/lanes32_tile.wgsl | 117 ++++
wai/wai-quantum/src/quantum_fabric/plan.rs | 567 ++++++++++++++++--
wai/wai-quantum/src/quantum_fabric/wgsl.rs | 368 +++++++++++-
wai/wai-quantum/src/quantum_fabric_webgpu.rs | 289 +++++++--
wai/wai-quantum/tests/webgpu_member.rs | 232 ++++++-
wai/wai-web/demo/README.md | 8 +-
wai/wai-web/demo/quantum_webgpu.html | 8 +-
wai/wai-web/demo/quantum_webgpu_worker.js | 61 +-
wai/wai-web/src/quantum_webgpu.rs | 21 +-
19 files changed, 1959 insertions(+), 295 deletions(-)
create mode 100644 wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml
index f930fac05..717013f7b 100644
--- a/.github/workflows/wai-conformance.yml
+++ b/.github/workflows/wai-conformance.yml
@@ -501,7 +501,10 @@ jobs:
quantum-wasip2:
name: quantum core and corpus under wasm32-wasip2 (wasmtime)
runs-on: linux
- timeout-minutes: 60
+ # The fabric's planner test runs fabric-v1 under every planner configuration in a
+ # debug wasm build, several minutes on its own; the timeout leaves it room rather
+ # than the test being skipped on wasm32.
+ timeout-minutes: 90
steps:
- uses: actions/checkout@v5
@@ -562,10 +565,10 @@ jobs:
done
# The fabric's groundwork for accelerator members (certification one case at
# a time, the fabric-v1 corpus and its pins, the 32-bit-lane words and their
- # range guard, the program a member runs, the WGSL kernel's host layout,
- # member ids and mutants), each by name: a module filter is satisfied by any
- # one test of the module, so a test dropped or compiled out on wasm32 would
- # otherwise go unnoticed.
+ # range guard, the program a member runs in every planner configuration, the
+ # WGSL kernels' host layouts, member ids and mutants), each by name: a module
+ # filter is satisfied by any one test of the module, so a test dropped or
+ # compiled out on wasm32 would otherwise go unnoticed.
for t in quantum_fabric::tests::certification_is_certify \
quantum_fabric::tests::fxmul_wide_is_the_exact_product \
quantum_fabric::tests::apply_words_is_apply \
@@ -580,6 +583,12 @@ jobs:
quantum_fabric::tests::fabric_v1_covers_every_dispatch_shape \
quantum_fabric::tests::gate_words_are_the_table \
quantum_fabric::plan::tests::execute_is_the_native_simulator \
+ quantum_fabric::plan::tests::every_configuration_is_the_native_simulator \
+ quantum_fabric::plan::tests::long_circuits_tile_as_they_run_gate_by_gate \
+ quantum_fabric::plan::tests::tiles_are_exact_on_any_words \
+ quantum_fabric::plan::tests::tiles_cover_every_path \
+ quantum_fabric::plan::tests::the_v1_tail_phases_are_not_skipped \
+ quantum_fabric::plan::tests::malformed_programs_are_refused \
quantum_fabric::wgsl::tests::implementation_id_is_pinned \
quantum_fabric::wgsl::tests::gate_record_layout \
quantum_fabric::wgsl::tests::grids_cover_every_item_once \
@@ -587,7 +596,12 @@ jobs:
quantum_fabric::wgsl::tests::member_ids_are_pinned \
quantum_fabric::wgsl::tests::the_cpu_reference_is_placed_first_when_nothing_is_metered \
quantum_fabric::wgsl::tests::mutants_apply_once \
- quantum_fabric::wgsl::tests::mutant_witnesses_exist; do
+ quantum_fabric::wgsl::tests::mutant_witnesses_exist \
+ quantum_fabric::wgsl::tests::tile_implementation_id_is_pinned \
+ quantum_fabric::wgsl::tests::tile_records_layout \
+ quantum_fabric::wgsl::tests::tile_grids_cover_every_tile_once \
+ quantum_fabric::wgsl::tests::tile_mutants_apply_once \
+ quantum_fabric::wgsl::tests::tile_mutant_witnesses_exist; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/wasip2-lib.log" \
|| { echo "::error::$t did not run"; exit 1; }
done
diff --git a/.github/workflows/wai-quantum-webgpu.yml b/.github/workflows/wai-quantum-webgpu.yml
index ba2724949..d7848c1f6 100644
--- a/.github/workflows/wai-quantum-webgpu.yml
+++ b/.github/workflows/wai-quantum-webgpu.yml
@@ -1,7 +1,10 @@
name: wai-quantum WebGPU member
# The fabric's WebGPU member: one WGSL gate kernel, compiled for every adapter a
-# runner has, certified against the CPU reference's pinned corpus digests.
+# runner has, certified against the CPU reference's pinned corpus digests. A
+# second implementation, tile runs (the same kernel followed by apply_tile, which
+# applies a run of gates tile by tile in one dispatch), is certified on its own
+# against the same digests, on every adapter and in the browser.
#
# Each member job declares the adapters its runner must have in
# WAI_QUANTUM_WEBGPU_EXPECT. The device test fails if the adapters found differ,
@@ -92,7 +95,7 @@ jobs:
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq --no-install-recommends spirv-tools
- - name: The shader under naga 30.0.1
+ - name: The shaders under naga 30.0.1
shell: bash
working-directory: wai/tools/quantum-wgsl-check
run: |
@@ -100,6 +103,7 @@ jobs:
cargo run --locked --release -- "$RUNNER_TEMP" 2>&1 | tee "$RUNNER_TEMP/wgsl-check.log"
grep -qx "quantum-wgsl-check: ok" "$RUNNER_TEMP/wgsl-check.log"
spirv-val --target-env vulkan1.1 "$RUNNER_TEMP/lanes32.spv"
+ spirv-val --target-env vulkan1.1 "$RUNNER_TEMP/lanes32_tile.spv"
- name: The member builds for a browser
working-directory: wai/wai-quantum
run: cargo build --locked --lib --target wasm32-unknown-unknown --no-default-features --features quantum_fabric_webgpu
@@ -125,6 +129,7 @@ jobs:
set -euo pipefail
cargo package --locked --list --allow-dirty > "$RUNNER_TEMP/package.txt"
grep -qx 'src/quantum_fabric/lanes32.wgsl' "$RUNNER_TEMP/package.txt"
+ grep -qx 'src/quantum_fabric/lanes32_tile.wgsl' "$RUNNER_TEMP/package.txt"
- name: MSRV 1.97.0
working-directory: wai/wai-quantum
run: cargo +1.97.0 check --locked --lib --features quantum_fabric_webgpu
@@ -250,6 +255,9 @@ jobs:
implementation="$(python3 -I -c 'import json, sys; print(json.load(open(sys.argv[1]))["implementation"])' "$f")"
grep -q "PINNED_IMPLEMENTATION: &str = \"$implementation\";" wai/wai-quantum/src/quantum_fabric/wgsl.rs \
|| { echo "::error::$implementation is not the pinned implementation id"; exit 1; }
+ tile="$(python3 -I -c 'import json, sys; print(json.load(open(sys.argv[1]))["tile_implementation"])' "$f")"
+ grep -q "PINNED_TILE_IMPLEMENTATION: &str = \"$tile\";" wai/wai-quantum/src/quantum_fabric/wgsl.rs \
+ || { echo "::error::$tile is not the pinned tile implementation id"; exit 1; }
# Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the crate
# must match exactly.
- name: wasm-bindgen CLI 0.2.127
@@ -285,9 +293,9 @@ jobs:
set -euo pipefail
WAI_QUANTUM_WEBGPU_REQUIRE=1 nice -n 10 node wai/quantum-conformance/browser_webgpu.mjs 2>&1 \
| tee "$RUNNER_TEMP/browser-webgpu.log"
- grep -qE '^browser webgpu: member=local/gpu-webgpu-[0-9a-f]{12} class=gpu certified bit-exact 4 of 4; ' \
+ grep -qE '^browser webgpu: member=local/gpu-webgpu-[0-9a-f]{12} class=gpu certified bit-exact 4 of 4; tiles certified bit-exact 4 of 4; ' \
"$RUNNER_TEMP/browser-webgpu.log" \
- || { echo "::error::no GPU member certified bit-exact 4 of 4 in the browser"; exit 1; }
+ || { echo "::error::no GPU member certified bit-exact 4 of 4, per-gate and in tile runs, in the browser"; exit 1; }
# Windows, when its runner is back (DX12, translator naga+dxbc, and Vulkan, as separate members):
# member-windows:
diff --git a/scripts/wai-quantum-webgpu-member.sh b/scripts/wai-quantum-webgpu-member.sh
index 9d9e91d91..459e07dfb 100755
--- a/scripts/wai-quantum-webgpu-member.sh
+++ b/scripts/wai-quantum-webgpu-member.sh
@@ -2,11 +2,13 @@
#
# wai-quantum-webgpu-member.sh
#
-# Runs the WebGPU member's device tests and fails unless (1) all of them ran and
-# passed, (2) the adapters found are exactly WAI_QUANTUM_WEBGPU_EXPECT, (3) each
-# declared adapter earned one bit-exact certificate per pinned corpus digest, and
-# (4) each declared adapter killed every shader mutant. These checks are what make
-# a filtered, skipped or adapter-less run fail instead of pass.
+# Runs the WebGPU member's device tests and fails unless (1) all 13 of them ran
+# and passed, (2) the adapters found are exactly WAI_QUANTUM_WEBGPU_EXPECT, (3) each
+# declared adapter earned one bit-exact certificate per pinned corpus digest under
+# each implementation (the per-gate kernel, wgsl-lanes32/, and tile runs,
+# wgsl-lanes32-tile/), and (4) each declared adapter killed every shader mutant of
+# both. These checks are what make a filtered, skipped or adapter-less run fail
+# instead of pass.
#
# WAI_QUANTUM_WEBGPU_EXPECT= [WAI_QUANTUM_WEBGPU_WIDE=1] \
# scripts/wai-quantum-webgpu-member.sh
@@ -21,8 +23,8 @@ export WAI_QUANTUM_WEBGPU_REPORT="$dir/webgpu-member.report"
cargo test --locked --features quantum_fabric_webgpu --test webgpu_member \
-- --nocapture --test-threads=1 2>&1 | tee "$log"
-grep -q "test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" "$log" \
- || { echo "::error::the member's 12 tests did not all run and pass"; exit 1; }
+grep -q "test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" "$log" \
+ || { echo "::error::the member's 13 tests did not all run and pass"; exit 1; }
grep -qxF "webgpu adapters: as declared ($WAI_QUANTUM_WEBGPU_EXPECT)" "$WAI_QUANTUM_WEBGPU_REPORT" \
|| { echo "::error::the adapters found are not the declared set"; exit 1; }
@@ -37,11 +39,15 @@ for key in $(tr ',' '\n' <<<"$WAI_QUANTUM_WEBGPU_EXPECT" | sort -u); do
want=$(tr ',' '\n' <<<"$WAI_QUANTUM_WEBGPU_EXPECT" | grep -cxF "$key")
for p in "${pins[@]}"; do
set -- $p
- n=$(grep -cE "^webgpu certified: key=$key member=local/[a-z]+-[a-z0-9]+-[0-9a-f]{12} kernel=$1 corpus=$2 implementation=wgsl-lanes32/[0-9a-f]{16} parity=bit-exact cases=[0-9]+ digest=$3$" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
- [ "$n" -eq "$want" ] || { echo "::error::$key: $n bit-exact $2 certificates for $1, $want expected"; exit 1; }
+ for implementation in wgsl-lanes32 wgsl-lanes32-tile; do
+ n=$(grep -cE "^webgpu certified: key=$key member=local/[a-z]+-[a-z0-9]+-[0-9a-f]{12} kernel=$1 corpus=$2 implementation=$implementation/[0-9a-f]{16} parity=bit-exact cases=[0-9]+ digest=$3$" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
+ [ "$n" -eq "$want" ] || { echo "::error::$key: $n bit-exact $2 certificates for $1 under $implementation, $want expected"; exit 1; }
+ done
done
n=$(grep -cxF "webgpu mutants: key=$key killed 7 of 7" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
[ "$n" -eq "$want" ] || { echo "::error::$key: a mutant survived, or the mutant test did not run"; exit 1; }
+ n=$(grep -cxF "webgpu tile mutants: key=$key killed 3 of 3" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
+ [ "$n" -eq "$want" ] || { echo "::error::$key: a tile mutant survived, or the mutant test did not run"; exit 1; }
if [ "${WAI_QUANTUM_WEBGPU_WIDE:-0}" = 1 ] && [ "${key%%-*}" = gpu ]; then
grep -qE "^webgpu wide: key=$key n=2[56] matches native$" "$WAI_QUANTUM_WEBGPU_REPORT" \
|| { echo "::error::$key: no wide run at the member's full size"; exit 1; }
diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md
index f15dfec86..8f766e918 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -79,6 +79,27 @@ Additive. The default features, `full`, and every existing pin and digest are un
if the adapters found differ, if any test did not run, if a declared adapter lacks a
bit-exact certificate for each pinned digest, or if a shader mutant survives. The
`wasm32-wasip2` job requires the fabric's new unit tests by name.
+- **Tile runs: a second implementation, `wgsl-lanes32-tile/de6fb388454a0144`.**
+ `plan::Config` (`PER_GATE`, `TILED`; `tile_size`, `run_cap`), `plan::Step::Tile`,
+ `compile_with`, `schedule`, `execute_from`, `PlannedGate::{new, is_identity}` and
+ `TILE_BITS_MAX`; `PLAN_REVISION` is 2. A tiled plan applies each maximal run of
+ consecutive gates that act inside a tile (diagonal gates on any target, others with
+ their target inside the tile) to tiles of up to 1024 amplitudes, in op-log order,
+ and leaves out exact identities only (circuit/1's `P(19..=32)` is `2^30 − 1`, not the
+ identity, and stays). The CPU executor mirrors a tile kernel's index arithmetic and
+ equals the per-gate program on any words; every configuration certifies with the
+ lanes32 pins. `wgsl::TILE_SHADER` (`lanes32.wgsl` followed by `lanes32_tile.wgsl`,
+ entry point `apply_tile`: one workgroup of 256 per tile, the tile updated in place in
+ the state planes, a storage barrier between gates, the tile size in the run record,
+ never in the text) has its own id (`tile_implementation_id`), records
+ (`tile_gate_record`, `tile_run_record`), grid (`tile_grid`) and mutants
+ (`TILE_MUTANTS`, with the detectors `Race` and `Malformed`, `race_witness`,
+ `malformed_tile_run`). `WebGpuMember::tiled()` gives the same device running it,
+ probed and certified on its own; the per-gate member stays the default and the
+ fallback. The checker holds `TILE_SHADER` to its own rules, barrier placement
+ included, and its SPIR-V to the validator; the member's device tests, the member
+ script and the browser leg require the four pinned digests under both
+ implementations and every tile mutant caught.
- **The member in a browser.** wai-web's feature `quantum_webgpu` binds the member for JS
(`quantum_webgpu_discover`, `QuantumWebGpu`: `certify`, `statevector_hash_hex`) in a
bundle of its own (`--out-name wai_quantum_webgpu`); the default build and its bundle
diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md
index d1d08dac4..372f036fa 100644
--- a/wai/quantum-conformance/README.md
+++ b/wai/quantum-conformance/README.md
@@ -68,9 +68,11 @@ Serves `wai-web/demo` and this directory on 127.0.0.1 and opens
bundle built into `wai-web/demo/pkg-webgpu` (`wai-web/demo/README.md`). The gate
kernel runs on the page's WebGPU adapter, compiled by the browser's own WGSL
compiler. The page must certify the kernel bit-exact for both gatesets on random-v1
-and fabric-v1 with the digests `fabric/expected.json` pins (the CPU reference's), run
-every vector of both trees to its statevector hash, refuse every reject file with its
-listed class, and refuse a circuit wider than the member holds. Without
+and fabric-v1 with the digests `fabric/expected.json` pins (the CPU reference's), and
+run every vector of both trees to its statevector hash, on the per-gate implementation
+and again on the tile implementation `fabric/expected.json` names (the browser's
+compiler holds its barrier to WGSL's uniformity rules); refuse every reject file with
+its listed class; and refuse a circuit wider than the member holds. Without
`WAI_QUANTUM_WEBGPU_REQUIRE=1`, a page offered no adapter is reported and passes. CI
runs it on macOS (`wai-quantum-webgpu.yml`, job `browser`).
diff --git a/wai/quantum-conformance/browser_webgpu.mjs b/wai/quantum-conformance/browser_webgpu.mjs
index b2555f3b8..8936e1e5d 100644
--- a/wai/quantum-conformance/browser_webgpu.mjs
+++ b/wai/quantum-conformance/browser_webgpu.mjs
@@ -20,6 +20,10 @@
// 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
@@ -28,7 +32,7 @@
//
// On success it prints
//
-// browser webgpu: member= class= certified bit-exact 4 of 4; vectors of ; rejects of
+// 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.
@@ -144,35 +148,53 @@ try {
report(`implementation ${member.implementation}`,
member.implementation === EXPECTED.implementation ? null : `fabric/expected.json pins ${EXPECTED.implementation}`);
- // The four certificates.
- let certified = 0;
- for (const want of EXPECTED.certificates) {
- const label = `${want.kernel} ${want.corpus}`;
- const got = of("certificate").find((m) => m.label === label);
- 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 !== member.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++;
- }
+ // 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);
- // Every vector.
- let vectors = 0;
- for (const [label, want] of Object.entries(VECTORS)) {
- const got = of("vector").find((m) => m.label === label);
- const problem = !got ? "not reported" : got.error ? `refused: ${got.error}` : got.got !== want ? `browser=${got.got} corpus=${want}` : null;
- report(label, problem);
- if (!problem) vectors++;
- }
+ // 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;
@@ -204,7 +226,8 @@ try {
const n = Object.keys(VECTORS).length;
const r = Object.keys(REJECTS).length;
if (fail === 0) {
- console.log(`browser webgpu: member=${member.id} class=${member.class} certified bit-exact ${certified} of ${EXPECTED.certificates.length}; vectors ${vectors} of ${n}; rejects ${rejects} of ${r}`);
+ 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`);
}
diff --git a/wai/quantum-conformance/fabric/expected.json b/wai/quantum-conformance/fabric/expected.json
index 8f863b2b1..f336a983e 100644
--- a/wai/quantum-conformance/fabric/expected.json
+++ b/wai/quantum-conformance/fabric/expected.json
@@ -1,5 +1,6 @@
{
"implementation": "wgsl-lanes32/dc8e19f61e0e87ab",
+ "tile_implementation": "wgsl-lanes32-tile/de6fb388454a0144",
"certificates": [
{
"gateset": 1,
diff --git a/wai/tools/quantum-wgsl-check/src/main.rs b/wai/tools/quantum-wgsl-check/src/main.rs
index 8752a80a5..e25d0b99c 100644
--- a/wai/tools/quantum-wgsl-check/src/main.rs
+++ b/wai/tools/quantum-wgsl-check/src/main.rs
@@ -1,37 +1,52 @@
-//! `quantum-wgsl-check`: holds the WGSL gate kernel of `wai-quantum` to the
-//! rules its bit-exactness argument rests on (the module documentation of
-//! `wai_quantum::quantum_fabric::wgsl`), on naga's IR, and translates it to
+//! `quantum-wgsl-check`: holds the WGSL gate kernels of `wai-quantum` to the
+//! rules their bit-exactness arguments rest on (the module documentation of
+//! `wai_quantum::quantum_fabric::wgsl`), on naga's IR, and translates them to
//! MSL, SPIR-V and HLSL.
//!
//! ```text
-//! quantum-wgsl-check [--shader ] []
+//! quantum-wgsl-check [--shader ] [--tile-shader ] []
//! ```
//!
-//! Without `--shader`, the text checked is
-//! `wai_quantum::quantum_fabric::wgsl::SHADER`, the exact constant the crate
-//! compiles. `--shader ` checks that file's text instead, so that a
-//! planted violation can be shown to fail; the mutants are then applied to that
-//! text. With a directory, the SPIR-V is written there as `lanes32.spv`, for
-//! `spirv-val`.
+//! Two texts are checked, each under its own rules: `SHADER`, the per-gate
+//! kernel, and `TILE_SHADER`, the same kernel followed by the tile runs. By
+//! default they are `wai_quantum::quantum_fabric::wgsl::SHADER` and
+//! `TILE_SHADER`, the exact constants the crate compiles. `--shader ` or
+//! `--tile-shader ` checks that file's text in place of one, so that a
+//! planted violation can be shown to fail; its mutants are then applied to
+//! that text. With a directory, the SPIR-V is written there as `lanes32.spv`
+//! and `lanes32_tile.spv`, for `spirv-val`.
//!
-//! The checks, in order; the first failure ends the run with exit status 1:
+//! The checks, in order, for each text; the first failure ends the run with
+//! exit status 1:
//!
//! 1. The text parses, and validates with every validation flag and no
//! capability.
-//! 2. The entry points are exactly `apply` and `probe`, both compute, both with
-//! workgroup size `[64, 1, 1]`.
-//! 3. The globals are bindings 0 to 5 of group 0: storage read-write at 0, 1, 2
-//! and 4, uniform at 3 and 5. No workgroup variable.
+//! 2. The entry points are exactly the expected ones, all compute: `apply` and
+//! `probe` with workgroup size `[64, 1, 1]`, and in `TILE_SHADER`
+//! `apply_tile` with `[256, 1, 1]`.
+//! 3. The bound globals are group 0: storage read-write at 0, 1, 2 and 4,
+//! uniform at 3 and 5, and in `TILE_SHADER` storage read-only at 6 and
+//! uniform at 7. Neither has a workgroup variable (the SPIR-V naga writes
+//! for a workgroup array or structure carries layout decorations a Vulkan
+//! validator rejects; were one allowed, `workgroup_bytes` bounds it).
//! 4. Every scalar type, in the type arena and in the type of every
//! expression, is `bool` or a 32-bit unsigned integer.
//! 5. No conversion or bitcast, no division or remainder, no built-in math
//! function; every shift amount is a `u32` literal from 1 to 31, or a
//! constant initialised with one.
-//! 6. Every atomic operation is `atomicOr` or `atomicMin`; no barrier.
+//! 6. Every atomic operation is `atomicOr` or `atomicMin`. `SHADER` has no
+//! barrier. In `TILE_SHADER`, every barrier is a `storageBarrier()` in
+//! `apply_tile`, placed where the uniformity argument puts it: at the top
+//! level of the entry point, or at the top level of the body of a `for`
+//! loop at the top level, a loop with no `break` but its condition's and no
+//! `continue` or `return`. (Naga does not check that a barrier is in
+//! uniform control flow; this checks the placement the argument relies on,
+//! and a browser's WGSL compiler checks the uniformity itself.)
//! 7. The module translates to MSL 2.4, to SPIR-V, and to HLSL shader model
//! 5.1, the model the DX12 member compiles for.
-//! 8. Every mutant of `wgsl::MUTANTS` changes the text, parses and validates.
-//! 9. The implementation id is printed (for `SHADER` only).
+//! 8. Every mutant (`wgsl::MUTANTS` for `SHADER`, `wgsl::TILE_MUTANTS` for
+//! `TILE_SHADER`) changes the text, parses and validates.
+//! 9. The implementation id is printed (for the crate's own text only).
//!
//! The last line of a passing run is `quantum-wgsl-check: ok`.
@@ -41,23 +56,69 @@ use std::process::ExitCode;
use naga::back::{hlsl, msl, spv};
use naga::valid::{Capabilities, ModuleInfo, ValidationFlags, Validator};
-use naga::{AddressSpace, Arena, AtomicFunction, BinaryOperator, Block, Expression, Handle, Literal, Module, Scalar, ScalarKind, ShaderStage, Statement, StorageAccess, TypeInner};
-use wai_quantum::quantum_fabric::wgsl;
+use naga::{AddressSpace, Arena, AtomicFunction, Barrier, BinaryOperator, Block, Expression, Handle, Literal, Module, Scalar, ScalarKind, ShaderStage, Statement, StorageAccess, TypeInner};
+use wai_quantum::quantum_fabric::wgsl::{self, Mutant};
const NAME: &str = "quantum-wgsl-check";
type Check = Result;
+const READ_WRITE: &str = "storage read-write";
+const READ: &str = "storage read";
+const UNIFORM: &str = "uniform";
+
+/// What one text is held to.
+struct Rules {
+ /// The constant's name, as the output says it.
+ label: &'static str,
+ /// The SPIR-V file written for `spirv-val`.
+ spv: &'static str,
+ /// The entry points and their workgroup sizes along x.
+ entry_points: &'static [(&'static str, u32)],
+ /// The bound globals of group 0.
+ bindings: &'static [(u32, &'static str)],
+ /// Bytes of workgroup memory allowed; zero forbids workgroup variables.
+ workgroup_bytes: u32,
+ /// The one entry point that may hold barriers, if any, and the one kind
+ /// of barrier it may hold.
+ barriers_in: Option<(&'static str, Barrier)>,
+ mutants: &'static [Mutant],
+ implementation: fn() -> String,
+}
+
+const PER_GATE: Rules = Rules {
+ label: "SHADER",
+ spv: "lanes32.spv",
+ entry_points: &[("apply", wgsl::WORKGROUP), ("probe", wgsl::WORKGROUP)],
+ bindings: &[(0, READ_WRITE), (1, READ_WRITE), (2, READ_WRITE), (3, UNIFORM), (4, READ_WRITE), (5, UNIFORM)],
+ workgroup_bytes: 0,
+ barriers_in: None,
+ mutants: &wgsl::MUTANTS,
+ implementation: wgsl::implementation_id,
+};
+
+const TILE: Rules = Rules {
+ label: "TILE_SHADER",
+ spv: "lanes32_tile.spv",
+ entry_points: &[("apply", wgsl::WORKGROUP), ("apply_tile", wgsl::TILE_WORKGROUP), ("probe", wgsl::WORKGROUP)],
+ bindings: &[(0, READ_WRITE), (1, READ_WRITE), (2, READ_WRITE), (3, UNIFORM), (4, READ_WRITE), (5, UNIFORM), (6, READ), (7, UNIFORM)],
+ workgroup_bytes: 0,
+ barriers_in: Some(("apply_tile", Barrier::STORAGE)),
+ mutants: &wgsl::TILE_MUTANTS,
+ implementation: wgsl::tile_implementation_id,
+};
+
fn main() -> ExitCode {
let args: Vec = std::env::args().skip(1).collect();
- let (shader, out) = match parse_args(&args) {
+ let (shader, tile_shader, out) = match parse_args(&args) {
Ok(a) => a,
Err(e) => {
- eprintln!("{NAME}: {e}\nusage: {NAME} [--shader ] []");
+ eprintln!("{NAME}: {e}\nusage: {NAME} [--shader ] [--tile-shader ] []");
return ExitCode::from(2);
}
};
- match run(shader.as_deref(), out.as_deref()) {
+ let result = run(&PER_GATE, wgsl::SHADER, shader.as_deref(), out.as_deref()).and_then(|()| run(&TILE, wgsl::TILE_SHADER, tile_shader.as_deref(), out.as_deref()));
+ match result {
Ok(()) => {
println!("{NAME}: ok");
ExitCode::SUCCESS
@@ -69,12 +130,16 @@ fn main() -> ExitCode {
}
}
-fn parse_args(args: &[String]) -> Check<(Option, Option)> {
- let (mut shader, mut out) = (None, None);
+type Args = (Option, Option, Option);
+
+fn parse_args(args: &[String]) -> Check {
+ let (mut shader, mut tile_shader, mut out) = (None, None, None);
let mut it = args.iter();
while let Some(a) = it.next() {
if a == "--shader" {
shader = Some(PathBuf::from(it.next().ok_or("--shader needs a file")?));
+ } else if a == "--tile-shader" {
+ tile_shader = Some(PathBuf::from(it.next().ok_or("--tile-shader needs a file")?));
} else if a.starts_with("--") {
return Err(format!("unknown option {a}"));
} else if out.is_none() {
@@ -83,39 +148,48 @@ fn parse_args(args: &[String]) -> Check<(Option, Option)> {
return Err(format!("unexpected argument {a}"));
}
}
- Ok((shader, out))
+ Ok((shader, tile_shader, out))
}
-fn run(shader: Option<&Path>, out: Option<&Path>) -> Check {
- let text = match shader {
+fn run(rules: &Rules, own: &str, file: Option<&Path>, out: Option<&Path>) -> Check {
+ let label = rules.label;
+ let text = match file {
None => {
- println!("{NAME}: checking wai_quantum::quantum_fabric::wgsl::SHADER");
- wgsl::SHADER.to_string()
+ println!("{NAME}: {label}: checking wai_quantum::quantum_fabric::wgsl::{label}");
+ own.to_string()
}
Some(p) => {
- println!("{NAME}: checking {} in place of the compiled SHADER", p.display());
+ println!("{NAME}: {label}: checking {} in place of the compiled {label}", p.display());
std::fs::read_to_string(p).map_err(|e| format!("reading {}: {e}", p.display()))?
}
};
- let (module, info) = compile(&text)?;
- println!("{NAME}: 1. parsed and validated (every validation flag, no capability)");
- entry_points(&module)?;
- println!("{NAME}: 2. entry points apply and probe: compute, workgroup size [64, 1, 1]");
- globals(&module)?;
- println!("{NAME}: 3. group 0: storage read-write at 0, 1, 2, 4; uniform at 3, 5; no workgroup variable");
- types(&module, &info)?;
- println!("{NAME}: 4. every scalar is bool or u32");
- expressions(&module)?;
- println!("{NAME}: 5. no conversion, division, remainder or math built-in; every shift amount a literal in 1..=31");
+ let at = |e: String| format!("{label}: {e}");
+ let (module, info) = compile(&text).map_err(at)?;
+ println!("{NAME}: {label}: 1. parsed and validated (every validation flag, no capability)");
+ entry_points(rules, &module).map_err(at)?;
+ let eps: Vec = rules.entry_points.iter().map(|(n, x)| format!("{n} [{x}, 1, 1]")).collect();
+ println!("{NAME}: {label}: 2. compute entry points {}", eps.join(", "));
+ let workgroup = globals(rules, &module).map_err(at)?;
+ let bound: Vec = rules.bindings.iter().map(|(b, s)| format!("{b} {s}")).collect();
+ println!("{NAME}: {label}: 3. group 0: {}; workgroup memory {workgroup} bytes (at most {})", bound.join(", "), rules.workgroup_bytes);
+ types(&module, &info).map_err(at)?;
+ println!("{NAME}: {label}: 4. every scalar is bool or u32");
+ expressions(&module).map_err(at)?;
+ println!("{NAME}: {label}: 5. no conversion, division, remainder or math built-in; every shift amount a literal in 1..=31");
+ let mut barriers = 0;
for (name, f) in functions(&module) {
- statements(&f.body, name)?;
+ let allowed = rules.barriers_in.filter(|(ep, _)| *ep == name).map(|(_, kind)| kind);
+ statements(&f.body, name, allowed, Depth::Top, &mut barriers).map_err(at)?;
}
- println!("{NAME}: 6. atomics are atomicOr and atomicMin only; no barrier");
- translate(&module, &info, out)?;
- mutants(&text, shader.is_some())?;
- println!("{NAME}: 8. every mutant changes the text, parses and validates");
- if shader.is_none() {
- println!("{NAME}: implementation {}", wgsl::implementation_id());
+ match rules.barriers_in {
+ None => println!("{NAME}: {label}: 6. atomics are atomicOr and atomicMin only; no barrier"),
+ Some((ep, kind)) => println!("{NAME}: {label}: 6. atomics are atomicOr and atomicMin only; {barriers} barrier(s) of {kind:?}, all in {ep} at the top level of its body or of a top-level for loop without break, continue or return"),
+ }
+ translate(rules, &module, &info, out).map_err(at)?;
+ mutants(rules, own, &text, file.is_some()).map_err(at)?;
+ println!("{NAME}: {label}: 8. every mutant changes the text, parses and validates");
+ if file.is_none() {
+ println!("{NAME}: {label}: implementation {}", (rules.implementation)());
}
Ok(())
}
@@ -128,34 +202,46 @@ fn compile(text: &str) -> Check<(Module, ModuleInfo)> {
}
/// Step 2.
-fn entry_points(module: &Module) -> Check {
+fn entry_points(rules: &Rules, module: &Module) -> Check {
let mut names: Vec<&str> = module.entry_points.iter().map(|e| e.name.as_str()).collect();
names.sort_unstable();
- if names != ["apply", "probe"] {
- return Err(format!("2. the entry points are {names:?}, not [\"apply\", \"probe\"]"));
+ let mut want: Vec<&str> = rules.entry_points.iter().map(|(n, _)| *n).collect();
+ want.sort_unstable();
+ if names != want {
+ return Err(format!("2. the entry points are {names:?}, not {want:?}"));
}
for e in &module.entry_points {
if e.stage != ShaderStage::Compute {
return Err(format!("2. entry point `{}` is a {:?} stage", e.name, e.stage));
}
- if e.workgroup_size != [wgsl::WORKGROUP, 1, 1] || e.workgroup_size_overrides.is_some() {
- return Err(format!("2. entry point `{}` has workgroup size {:?} (overrides {:?})", e.name, e.workgroup_size, e.workgroup_size_overrides));
+ let x = rules.entry_points.iter().find(|(n, _)| *n == e.name).map(|(_, x)| *x).unwrap_or(0);
+ if e.workgroup_size != [x, 1, 1] || e.workgroup_size_overrides.is_some() {
+ return Err(format!("2. entry point `{}` has workgroup size {:?} (overrides {:?}), not [{x}, 1, 1]", e.name, e.workgroup_size, e.workgroup_size_overrides));
}
}
Ok(())
}
-/// Step 3.
-fn globals(module: &Module) -> Check {
- const READ_WRITE: &str = "storage read-write";
- const UNIFORM: &str = "uniform";
- let want = BTreeMap::from([(0, READ_WRITE), (1, READ_WRITE), (2, READ_WRITE), (3, UNIFORM), (4, READ_WRITE), (5, UNIFORM)]);
+/// Step 3. Returns the bytes of workgroup memory.
+fn globals(rules: &Rules, module: &Module) -> Check {
+ let want: BTreeMap = rules.bindings.iter().copied().collect();
let mut found = BTreeMap::new();
+ let mut workgroup = 0u32;
for (_, v) in module.global_variables.iter() {
let name = v.name.as_deref().unwrap_or("(unnamed)");
let space = match v.space {
- AddressSpace::WorkGroup => return Err(format!("3. `var {name}`: the kernel holds no workgroup memory")),
+ AddressSpace::WorkGroup => {
+ if rules.workgroup_bytes == 0 {
+ return Err(format!("3. `var {name}`: the kernel holds no workgroup memory"));
+ }
+ if v.binding.is_some() {
+ return Err(format!("3. workgroup variable `{name}` has a binding"));
+ }
+ workgroup += module.types[v.ty].inner.size(module.to_ctx());
+ continue;
+ }
AddressSpace::Storage { access } if access.contains(StorageAccess::LOAD | StorageAccess::STORE) => READ_WRITE,
+ AddressSpace::Storage { access } if access == StorageAccess::LOAD => READ,
AddressSpace::Uniform => UNIFORM,
other => return Err(format!("3. global `{name}` is in address space {other:?}")),
};
@@ -170,7 +256,10 @@ fn globals(module: &Module) -> Check {
if found != want {
return Err(format!("3. the bindings are {found:?}, not {want:?}"));
}
- Ok(())
+ if workgroup > rules.workgroup_bytes {
+ return Err(format!("3. {workgroup} bytes of workgroup memory, more than {}", rules.workgroup_bytes));
+ }
+ Ok(workgroup)
}
fn scalar_ok(s: Scalar) -> bool {
@@ -253,8 +342,66 @@ fn expressions(module: &Module) -> Check {
Ok(())
}
+/// Where a statement sits, for the barrier rule of step 6.
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum Depth {
+ /// The top level of a function's body.
+ Top,
+ /// The top level of the body of a `for` loop that is at the top level.
+ LoopBody,
+ /// Anywhere else.
+ Nested,
+}
+
+/// Whether a block holds a statement that leaves a loop or the function
+/// (`break`, `continue`, `return`), outside nested loops for `break` and
+/// `continue`.
+fn exits(block: &Block, in_nested_loop: bool) -> Option<&'static str> {
+ for s in block.iter() {
+ let found = match s {
+ Statement::Return { .. } => Some("return"),
+ Statement::Break if !in_nested_loop => Some("break"),
+ Statement::Continue if !in_nested_loop => Some("continue"),
+ Statement::Block(b) => exits(b, in_nested_loop),
+ Statement::If { accept, reject, .. } => exits(accept, in_nested_loop).or_else(|| exits(reject, in_nested_loop)),
+ // A `break` in a switch leaves the switch only.
+ Statement::Switch { cases, .. } => cases.iter().find_map(|c| exits(&c.body, in_nested_loop).filter(|e| *e != "break")),
+ Statement::Loop { body, continuing, .. } => exits(body, true).or_else(|| exits(continuing, true)).filter(|e| *e == "return"),
+ _ => None,
+ };
+ if found.is_some() {
+ return found;
+ }
+ }
+ None
+}
+
+/// Whether a block holds a barrier anywhere.
+fn has_barrier(block: &Block) -> bool {
+ block.iter().any(|s| match s {
+ Statement::ControlBarrier(_) | Statement::MemoryBarrier(_) => true,
+ Statement::Block(b) => has_barrier(b),
+ Statement::If { accept, reject, .. } => has_barrier(accept) || has_barrier(reject),
+ Statement::Switch { cases, .. } => cases.iter().any(|c| has_barrier(&c.body)),
+ Statement::Loop { body, continuing, .. } => has_barrier(body) || has_barrier(continuing),
+ _ => false,
+ })
+}
+
+/// The body of a `for` loop as naga lowers it: emits, then
+/// `if (cond) {} else { break; }`, then the loop's own block. Returns that
+/// block when the loop has this form.
+fn for_body(body: &Block) -> Option<&Block> {
+ let rest: Vec<&Statement> = body.iter().skip_while(|s| matches!(s, Statement::Emit(_))).collect();
+ match rest.as_slice() {
+ [Statement::If { accept, reject, .. }, Statement::Block(user)] if accept.is_empty() && reject.len() == 1 && matches!(reject.iter().next(), Some(Statement::Break)) => Some(user),
+ _ => None,
+ }
+}
+
/// Step 6, recursively through every block.
-fn statements(block: &Block, name: &str) -> Check {
+fn statements(block: &Block, name: &str, barrier_kind: Option, depth: Depth, barriers: &mut u32) -> Check {
+ let barriers_allowed = barrier_kind.is_some();
for s in block.iter() {
match s {
Statement::Atomic { fun, .. } => {
@@ -262,20 +409,49 @@ fn statements(block: &Block, name: &str) -> Check {
return Err(format!("6. in `{name}`: the atomic operation {fun:?}"));
}
}
- Statement::ControlBarrier(_) | Statement::MemoryBarrier(_) => return Err(format!("6. in `{name}`: a barrier")),
- Statement::Block(b) => statements(b, name)?,
+ Statement::ControlBarrier(b) => {
+ if !barriers_allowed {
+ return Err(format!("6. in `{name}`: a barrier"));
+ }
+ if Some(*b) != barrier_kind {
+ return Err(format!("6. in `{name}`: a barrier of {b:?}, not of {barrier_kind:?}"));
+ }
+ if depth == Depth::Nested {
+ return Err(format!("6. in `{name}`: a barrier nested below the top level of the body or of a top-level for loop"));
+ }
+ *barriers += 1;
+ }
+ Statement::MemoryBarrier(_) => return Err(format!("6. in `{name}`: a memory barrier")),
+ Statement::Block(b) => statements(b, name, barrier_kind, if depth == Depth::Top { Depth::Top } else { Depth::Nested }, barriers)?,
Statement::If { accept, reject, .. } => {
- statements(accept, name)?;
- statements(reject, name)?;
+ statements(accept, name, barrier_kind, Depth::Nested, barriers)?;
+ statements(reject, name, barrier_kind, Depth::Nested, barriers)?;
}
Statement::Switch { cases, .. } => {
for c in cases {
- statements(&c.body, name)?;
+ statements(&c.body, name, barrier_kind, Depth::Nested, barriers)?;
}
}
- Statement::Loop { body, continuing, .. } => {
- statements(body, name)?;
- statements(continuing, name)?;
+ Statement::Loop { body, continuing, break_if } => {
+ // A barrier in a loop is allowed only in a top-level `for`
+ // loop, at the top level of its own block, and only when
+ // nothing but the condition's `break` leaves the loop.
+ let user = for_body(body).filter(|_| depth == Depth::Top && break_if.is_none());
+ if barriers_allowed && (has_barrier(body) || has_barrier(continuing)) {
+ let Some(user) = user else {
+ return Err(format!("6. in `{name}`: a barrier in a loop that is not a top-level for loop"));
+ };
+ if let Some(e) = exits(user, false).or_else(|| exits(continuing, false)) {
+ return Err(format!("6. in `{name}`: a `{e}` in a loop that holds a barrier"));
+ }
+ if has_barrier(continuing) {
+ return Err(format!("6. in `{name}`: a barrier in a loop's continuing block"));
+ }
+ statements(user, name, barrier_kind, Depth::LoopBody, barriers)?;
+ } else {
+ statements(body, name, barrier_kind, Depth::Nested, barriers)?;
+ statements(continuing, name, barrier_kind, Depth::Nested, barriers)?;
+ }
}
_ => {}
}
@@ -284,39 +460,43 @@ fn statements(block: &Block, name: &str) -> Check {
}
/// Step 7.
-fn translate(module: &Module, info: &ModuleInfo, out: Option<&Path>) -> Check {
+fn translate(rules: &Rules, module: &Module, info: &ModuleInfo, out: Option<&Path>) -> Check {
+ let label = rules.label;
let options = msl::Options { lang_version: (2, 4), ..Default::default() };
msl::write_string(module, info, &options, &msl::PipelineOptions::default()).map_err(|e| format!("7. MSL: {e}"))?;
let words = spv::write_vec(module, info, &spv::Options::default(), None).map_err(|e| format!("7. SPIR-V: {e}"))?;
let mut hlsl_text = String::new();
// Shader model 5.1 (the default), the model the DX12 member compiles for.
hlsl::Writer::new(&mut hlsl_text, &hlsl::Options::default(), &hlsl::PipelineOptions::default()).write(module, info, None).map_err(|e| format!("7. HLSL: {e}"))?;
- println!("{NAME}: 7. translated to MSL 2.4, SPIR-V and HLSL shader model 5.1");
+ println!("{NAME}: {label}: 7. translated to MSL 2.4, SPIR-V and HLSL shader model 5.1");
match out {
Some(dir) => {
- let path = dir.join("lanes32.spv");
+ let path = dir.join(rules.spv);
let bytes: Vec = words.iter().flat_map(|w| w.to_le_bytes()).collect();
std::fs::write(&path, bytes).map_err(|e| format!("7. writing {}: {e}", path.display()))?;
- println!("{NAME}: 7. SPIR-V written to {}", path.display());
+ println!("{NAME}: {label}: 7. SPIR-V written to {}", path.display());
}
- None => println!("{NAME}: 7. SPIR-V not written: no directory given"),
+ None => println!("{NAME}: {label}: 7. SPIR-V not written: no directory given"),
}
Ok(())
}
-/// Step 8. On `SHADER` the mutants are `wgsl::mutate`'s; on a file given with
-/// `--shader`, the same edits applied to its text, each `find` exactly once.
-fn mutants(text: &str, file: bool) -> Check {
- for m in &wgsl::MUTANTS {
- let mutated = if file {
- let n = text.matches(m.find).count();
- if n != 1 {
- return Err(format!("8. mutant {}: `{}` occurs {n} times", m.name, m.find));
+/// Step 8. Each mutant's `find` must occur exactly once in the text checked
+/// (the crate's own, or a file given in its place).
+fn mutants(rules: &Rules, own: &str, text: &str, file: bool) -> Check {
+ for m in rules.mutants {
+ let n = text.matches(m.find).count();
+ if n != 1 {
+ return Err(format!("8. mutant {}: `{}` occurs {n} times", m.name, m.find));
+ }
+ let mutated = text.replacen(m.find, m.replace, 1);
+ if !file {
+ // The crate's own mutant text is this one.
+ let crate_text = if rules.label == PER_GATE.label { wgsl::mutate(m) } else { wgsl::mutate_tile(m) };
+ if crate_text != mutated || text != own {
+ return Err(format!("8. mutant {}: the crate's mutant text is not this edit of {}", m.name, rules.label));
}
- text.replacen(m.find, m.replace, 1)
- } else {
- wgsl::mutate(m)
- };
+ }
if mutated == text {
return Err(format!("8. mutant {} does not change the text", m.name));
}
diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md
index 9018a42c2..b4a936433 100644
--- a/wai/wai-quantum/README.md
+++ b/wai/wai-quantum/README.md
@@ -94,7 +94,7 @@ never creates information.
| `quantum_channel` | **transport itself**: Kraus channels (bit flip, dephasing, depolarizing, amplitude damping), and linear-inversion tomography to see what actually arrived — with the law that a channel can only *lose* information, never create it |
| `quantum_qbom` | Quantum Bill of Materials — stage receipts into one manifest |
| `quantum_vml`, `quantum_phasor`, `quantum_qfhrr`, `quantum_kernel`, `quantum_qdata`, `quantum_phasor_meter` | the interference / phasor ML layer |
-| `quantum_fabric_webgpu` | **the fabric's WebGPU member**, opt-in and not in `full`: the 32-bit-lane gate kernel in WGSL, one dispatch per gate, through Metal, Vulkan or DX12 natively and through a browser's WebGPU on `wasm32-unknown-unknown`. Each adapter is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned. The one feature that reaches a platform interface |
+| `quantum_fabric_webgpu` | **the fabric's WebGPU member**, opt-in and not in `full`: the 32-bit-lane gate kernel in WGSL, one dispatch per gate, through Metal, Vulkan or DX12 natively and through a browser's WebGPU on `wasm32-unknown-unknown`; a second implementation applies runs of gates tile by tile, one dispatch per run, and is certified on its own. Each adapter is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned. The one feature that reaches a platform interface |
| `full` | everything |
Default features are `quantum`, `quantum_toolchain`, `quantum_receipt`.
diff --git a/wai/wai-quantum/src/quantum_fabric.rs b/wai/wai-quantum/src/quantum_fabric.rs
index f0b5d59c8..507a15814 100644
--- a/wai/wai-quantum/src/quantum_fabric.rs
+++ b/wai/wai-quantum/src/quantum_fabric.rs
@@ -37,10 +37,10 @@
//! a 32-bit member must check each result, as [`lanes::simulate_checked`]
//! does. [`lanes`] is that kernel written with only the operations such a
//! fabric has, and its certificate against the native simulator is bit-exact.
-//! [`plan`] is the program an accelerator member runs for a circuit, with a
-//! CPU executor of exactly that program, and [`wgsl`] is the same kernel in
-//! WGSL for a WebGPU member, with its host layout and the argument that it is
-//! bit-exact. The
+//! [`plan`] is the program an accelerator member runs for a circuit, one step
+//! per gate or in tile runs, with a CPU executor of exactly that program, and
+//! [`wgsl`] is the same kernel in WGSL for a WebGPU member, per gate and in
+//! tile runs, with its host layouts and the argument that it is bit-exact. The
//! floating-point modules can earn bit-exactness only on a fabric with 64-bit
//! floats and the same order of operations; elsewhere their parity is a
//! measured bound.
@@ -1218,20 +1218,12 @@ pub mod lanes {
if tbit == 0 || !tbit.is_power_of_two() || tbit > len / 2 || ctrl & tbit != 0 || ctrl & !(len - 1) != 0 {
return None;
}
- let e = |k: usize| w.m[k] as i32;
let mut fits = true;
for i in 0..len {
if i & tbit == 0 && i & ctrl == ctrl {
let j = i | tbit;
let (a0, a1) = (amps[i], amps[j]);
- let row = |r: usize| -> [i64; 2] {
- let (mr, mi, nr, ni) = (e(4 * r), e(4 * r + 1), e(4 * r + 2), e(4 * r + 3));
- [
- fx_exact(mr, a0[0]) - fx_exact(mi, a0[1]) + fx_exact(nr, a1[0]) - fx_exact(ni, a1[1]),
- fx_exact(mr, a0[1]) + fx_exact(mi, a0[0]) + fx_exact(nr, a1[1]) + fx_exact(ni, a1[0]),
- ]
- };
- let (o0, o1) = (row(0), row(1));
+ let (o0, o1) = (exact_row(w, 0, a0, a1), exact_row(w, 1, a0, a1));
fits &= o0.iter().chain(&o1).all(|v| i32::try_from(*v).is_ok());
// `as` keeps the low 32 bits.
amps[i] = [o0[0] as i32, o0[1] as i32];
@@ -1241,6 +1233,18 @@ pub mod lanes {
Some(fits)
}
+ /// Row `r` (0 or 1) of a gate's words on the pair `(a0, a1)`, exactly:
+ /// `m_r0·a0 + m_r1·a1`, every product the exact `fx` and the sums in 64
+ /// bits. The low 32 bits of each part are the lane's.
+ pub(super) fn exact_row(w: &GateWords, r: usize, a0: Lane, a1: Lane) -> [i64; 2] {
+ let e = |k: usize| w.m[k] as i32;
+ let (mr, mi, nr, ni) = (e(4 * r), e(4 * r + 1), e(4 * r + 2), e(4 * r + 3));
+ [
+ fx_exact(mr, a0[0]) - fx_exact(mi, a0[1]) + fx_exact(nr, a1[0]) - fx_exact(ni, a1[1]),
+ fx_exact(mr, a0[1]) + fx_exact(mi, a0[0]) + fx_exact(nr, a1[1]) + fx_exact(ni, a1[0]),
+ ]
+ }
+
/// Gates in order from `state`, as a 32-bit-lane member runs them: the
/// lanes, and the lowest position in `gates` of a gate whose exact result
/// left 32 bits. `None` if any gate's words are malformed for this state
@@ -1852,6 +1856,21 @@ mod tests {
}
}
+ /// The planner configurations every test of a program runs under: one
+ /// step per gate, the tiled default, and small tiles with short and long
+ /// runs, the identity skipped or kept.
+ pub(super) fn configurations() -> Vec {
+ let mut all = vec![plan::Config::PER_GATE, plan::Config::TILED];
+ for tile_bits in [1, 3] {
+ for run_max in [1, 7] {
+ for skip_identity in [true, false] {
+ all.push(plan::Config { tile_bits, run_max, skip_identity });
+ }
+ }
+ }
+ all
+ }
+
#[test]
fn a_rounded_product_is_never_negated() {
let v2 = [[759250126, 0], [1, 0], [759250126, 0], [1, 0]];
@@ -1863,6 +1882,11 @@ mod tests {
assert_eq!(lanes::simulate(&c).unwrap(), want);
let run = lanes::simulate_checked(&c, 0).unwrap();
assert_eq!((run.amps.as_slice(), run.out_of_range), (want.as_slice(), None));
+ // Every planner configuration, tile runs included, keeps the tie.
+ for cfg in configurations() {
+ let run = plan::execute(&plan::compile_with(&c, 0, &cfg).unwrap());
+ assert_eq!((run.amps.as_slice(), run.out_of_range), (want.as_slice(), None), "{cfg:?}");
+ }
// H with `m11·a_j` computed as `−(m01·a_j)`: one rounded product,
// negated, where the table rounds `−h·a_j` on its own.
let h = gs.inv_sqrt2() as i32;
diff --git a/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl b/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
new file mode 100644
index 000000000..828fa437c
--- /dev/null
+++ b/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
@@ -0,0 +1,117 @@
+
+// Tile runs: one workgroup owns one tile of run.size consecutive amplitudes of
+// the state and applies a run of gates to it in place, in op-log order, with a
+// storage barrier between gates. Each gate either has its target inside the
+// tile (tbit < size) or is diagonal (m01 = m10 = 0) and acts per amplitude.
+// Uses fx64, add64, sub64, fits, row and the bindings re, im, report of
+// lanes32.wgsl, which this file is appended to. No shift amount here is
+// anything but a literal; sizes and indices come multiplied, not shifted. No
+// workgroup memory: the tile stays in re and im, which no other workgroup of
+// the dispatch touches.
+//
+// Uniformity: the early returns depend only on run (a uniform buffer) and
+// workgroup_id. The barrier sits at the top level of the r loop, whose bound
+// run.count is a uniform-buffer value; no break, continue or return occurs
+// inside that loop, and no branch that depends on a gate or an invocation
+// holds a barrier.
+
+struct TileGate {
+ row0: vec4, // m00.re, m00.im, m01.re, m01.im
+ row1: vec4, // m10.re, m10.im, m11.re, m11.im
+ tbit: u32, // 1 << target
+ ctrl: u32, // the controls' bits, OR-ed
+ diag: u32, // 1: m01 = m10 = (0, 0)
+ index: u32, // the gate's position in the op-log
+}
+
+struct TileRun {
+ first: u32, // the run's first record in tile_gates
+ count: u32, // gates in the run, at least 1
+ size: u32, // amplitudes per tile: a power of two, at most TILE_MAX
+ x_groups: u32, // workgroups along x
+ tiles: u32, // tiles in the state: 2^n / size
+ pad0: u32,
+ pad1: u32,
+ pad2: u32,
+}
+
+@group(0) @binding(6) var tile_gates: array;
+@group(0) @binding(7) var run: TileRun;
+
+const TILE_MAX: u32 = 1024u;
+const TILE_LANES: u32 = 256u;
+const NONE: u32 = 0xffffffffu;
+const ZERO2: vec2 = vec2(0u, 0u);
+
+@compute @workgroup_size(256)
+fn apply_tile(@builtin(workgroup_id) wid: vec3,
+ @builtin(local_invocation_index) lid: u32) {
+ let size = run.size;
+ // A malformed run record is reported and never run (uniform: run is a uniform buffer).
+ if (size == 0u || size > TILE_MAX || (size & (size - 1u)) != 0u || run.count == 0u
+ || run.x_groups == 0u) {
+ atomicOr(&report[0], 4u);
+ return;
+ }
+ let tile = wid.y * run.x_groups + wid.x;
+ if (tile >= run.tiles) { return; } // workgroup-uniform
+ let base = tile * size;
+ let span = run.tiles * size; // 2^n
+ // A tile outside the state is reported, and nothing of it is touched.
+ let inside = base + size <= arrayLength(&re) && base + size <= arrayLength(&im);
+ if (!inside) { atomicOr(&report[0], 2u); }
+ var bad = NONE;
+ var malformed = false;
+ for (var r = 0u; r < run.count; r++) {
+ // A record outside tile_gates reads as malformed, never out of bounds.
+ let at = run.first + r;
+ let held = at < arrayLength(&tile_gates);
+ let h = tile_gates[select(0u, at, held)];
+ let shape = held && h.tbit != 0u && (h.tbit & (h.tbit - 1u)) == 0u && h.tbit < span
+ && (h.ctrl & h.tbit) == 0u && (h.ctrl & ~(span - 1u)) == 0u;
+ var ok = true;
+ if (shape && h.diag == 1u) {
+ for (var k = 0u; k < 4u; k++) {
+ let e = lid + TILE_LANES * k;
+ let gi = base + e;
+ if (inside && e < size && (gi & h.ctrl) == h.ctrl) {
+ let a = vec2(re[gi], im[gi]);
+ var o: vec4;
+ if ((gi & h.tbit) == 0u) { o = row(h.row0, a, ZERO2); } else { o = row(h.row1, ZERO2, a); }
+ re[gi] = o.y;
+ im[gi] = o.w;
+ if (!(fits(o.xy) && fits(o.zw))) { ok = false; }
+ }
+ }
+ } else if (shape && h.tbit < size) {
+ for (var k = 0u; k < 2u; k++) {
+ let q = lid + TILE_LANES * k;
+ if (inside && q < (size >> 1u)) {
+ let low = q & (h.tbit - 1u);
+ let gi = base + (((q ^ low) << 1u) | low); // q with a 0 inserted at the target bit
+ let gj = gi | h.tbit;
+ if ((gi & h.ctrl) == h.ctrl) {
+ let a0 = vec2(re[gi], im[gi]);
+ let a1 = vec2(re[gj], im[gj]);
+ let o0 = row(h.row0, a0, a1);
+ let o1 = row(h.row1, a0, a1);
+ re[gi] = o0.y;
+ im[gi] = o0.w;
+ re[gj] = o1.y;
+ im[gj] = o1.w;
+ if (!(fits(o0.xy) && fits(o0.zw) && fits(o1.xy) && fits(o1.zw))) { ok = false; }
+ }
+ }
+ }
+ } else {
+ malformed = true;
+ }
+ if (!ok) { bad = select(bad, h.index, h.index < bad); }
+ storageBarrier(); // this gate's writes before the next gate's reads
+ }
+ if (malformed) { atomicOr(&report[0], 4u); }
+ if (bad != NONE) {
+ atomicOr(&report[0], 1u);
+ atomicMin(&report[1], bad);
+ }
+}
diff --git a/wai/wai-quantum/src/quantum_fabric/plan.rs b/wai/wai-quantum/src/quantum_fabric/plan.rs
index f08abb962..71d6114d5 100644
--- a/wai/wai-quantum/src/quantum_fabric/plan.rs
+++ b/wai/wai-quantum/src/quantum_fabric/plan.rs
@@ -4,20 +4,106 @@
//! configuration.
//!
//! A [`Program`] holds the circuit's gates as [`GateWords`], in op-log order,
-//! and the steps that apply them. Each step here is one gate over the whole
-//! state. The arithmetic is [`lanes::apply_words`]: the stored lanes are the
-//! low 32 bits of the exact values, and a run reports the first op whose exact
-//! result left 32 bits, which no valid circuit of at most [`lanes::horizon`]
-//! gates can do.
+//! and the steps that apply them. A [`Step::Gate`] is one gate over the whole
+//! state. A [`Step::Tile`] is a run of consecutive gates applied tile by tile:
+//! the state is cut into tiles of `size` consecutive amplitudes, and each tile
+//! goes through every gate of the run, in op-log order, on its own, so that a
+//! member can apply a whole run in one dispatch, one group of invocations per
+//! tile. A gate joins a run when it acts inside a tile: a gate whose matrix is
+//! diagonal (`m01 = m10 = 0`) acts on each amplitude alone, whatever its
+//! target, and any other gate acts on pairs that lie inside one tile when its
+//! target's bit is below the tile's size. Every other gate is a step of its
+//! own. [`Config`] says how a
+//! circuit is planned: [`Config::PER_GATE`], one step per gate, is what
+//! [`compile`] makes; [`Config::TILED`] makes tile runs of up to 1024
+//! amplitudes.
+//!
+//! The arithmetic is [`lanes::apply_words`]'s in every configuration: the
+//! stored lanes are the low 32 bits of the exact values, and a run reports the
+//! first op whose exact result left 32 bits, which no valid circuit of at most
+//! [`lanes::horizon`] gates can do.
+//!
+//! # Why every configuration gives the same bits
+//!
+//! A planner may only move data exactly; it never changes what is computed.
+//!
+//! - **A tile run is its gates, one after another.** Each gate of a run acts
+//! on amplitudes of one tile only, so the tiles evolve independently, and
+//! within a tile the gates apply in op-log order to the values the previous
+//! gate left. Every pair or amplitude a gate updates is computed from the
+//! same inputs by the same exact rows as in [`lanes::apply_words`], and the
+//! same pairs are updated (a control is never the target, so both members of
+//! a pair pass or fail the control test together). Splitting a run in two is
+//! exact for the same reason.
+//! - **A diagonal gate acts per amplitude.** `apply_words` computes
+//! `a_i' = m00·a_i + m01·a_j` with `m01 = 0`, and the exact `fx(0, x)` is 0
+//! for every `x`, so the value is that of `m00·a_i + 0·0`; likewise
+//! `a_j' = 0·0 + m11·a_j`. The range check covers the same four parts.
+//! - **The identity is skipped** only when its words are exactly
+//! `[2^30, 0, 0, 0, 0, 0, 2^30, 0]`: `fx(2^30, x) = x` and `fx(0, x) = 0`
+//! for every 32-bit `x`, so the gate changes nothing and cannot leave 32
+//! bits. Circuit/1's `P(19..=32)` is `(2^30 − 1, 0)`, not the identity, and
+//! is never skipped.
+//! - **The report is the lowest op index** whose exact result left 32 bits.
+//! The lanes after a gate are the same in every configuration, so the set of
+//! gates whose exact result leaves 32 bits is the same, and so is its least
+//! element.
+//!
+//! What no configuration does, because each changes bits: negate a rounded
+//! product (`fx(−m, x)` differs from `−fx(m, x)` at a tie), multiply two
+//! gates' matrices into one, reorder gates (even ones on disjoint qubits), or
+//! skip a gate that is not exactly the identity.
use super::lanes::{self, GateWords, Lane};
use crate::quantum::{Circuit, MAX_QUBITS, ONE, QuantumError};
/// Bumped whenever the meaning of a [`Program`] changes. An implementation id
-/// that depends on more than [`Step::Gate`] includes it; the per-gate WGSL
-/// kernel's id ([`super::wgsl::implementation_id`]) versions the one step it
-/// runs with [`super::wgsl::HOST_LAYOUT`] instead.
-pub const PLAN_REVISION: u32 = 1;
+/// that runs more than [`Step::Gate`] includes it
+/// ([`super::wgsl::tile_implementation_id`]); the per-gate WGSL kernel's id
+/// ([`super::wgsl::implementation_id`]) versions the one step it runs with
+/// [`super::wgsl::HOST_LAYOUT`] instead. Revision 2 added [`Step::Tile`].
+pub const PLAN_REVISION: u32 = 2;
+
+/// The largest tile, as a power of two: 1024 amplitudes, four for each
+/// invocation of a tile kernel's workgroup of 256.
+pub const TILE_BITS_MAX: u8 = 10;
+
+/// The identity's words: `m00 = m11 = 2^30`, every other entry zero.
+const IDENTITY: [u32; 8] = [ONE as u32, 0, 0, 0, 0, 0, ONE as u32, 0];
+
+/// How a circuit is planned.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Config {
+ /// Amplitudes per tile: `2^min(n, tile_bits)`. Zero plans no tiles (one
+ /// step per gate). At most [`TILE_BITS_MAX`].
+ pub tile_bits: u8,
+ /// Gates per tile run, at most; further capped at `2^30 / 2^n` (at least
+ /// one), so that one run's work stays bounded on the widest states.
+ pub run_max: u32,
+ /// Leave out every gate whose words are exactly the identity's.
+ pub skip_identity: bool,
+}
+
+impl Config {
+ /// One [`Step::Gate`] per op, nothing skipped: what [`compile`] makes.
+ pub const PER_GATE: Config = Config { tile_bits: 0, run_max: 1, skip_identity: false };
+ /// Tiles of up to 1024 amplitudes, runs of up to 1024 gates, the identity
+ /// skipped.
+ pub const TILED: Config = Config { tile_bits: TILE_BITS_MAX, run_max: 1024, skip_identity: true };
+
+ /// Amplitudes per tile on `n_qubits`: `2^min(n, tile_bits)`, or 0 when
+ /// the configuration plans no tiles or the state has no qubits.
+ pub fn tile_size(&self, n_qubits: u8) -> u32 {
+ if self.tile_bits == 0 || n_qubits == 0 { 0 } else { 1 << n_qubits.min(self.tile_bits).min(TILE_BITS_MAX) }
+ }
+
+ /// Gates per tile run on `n_qubits`, at most: `run_max`, capped at
+ /// `2^30 / 2^n` and at least one.
+ pub fn run_cap(&self, n_qubits: u8) -> u32 {
+ let budget = if n_qubits >= 30 { 1 } else { (1u32 << 30) >> n_qubits };
+ self.run_max.min(budget).max(1)
+ }
+}
/// One gate of a program.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -29,12 +115,35 @@ pub struct PlannedGate {
pub diag: bool,
}
+impl PlannedGate {
+ /// The gate with these words at op-log position `index`; `diag` is read
+ /// from the words.
+ pub fn new(words: GateWords, index: u32) -> PlannedGate {
+ PlannedGate { words, index, diag: words.m[2..6] == [0; 4] }
+ }
+
+ /// Whether the words are exactly the identity's (whatever the target and
+ /// controls): the gate changes nothing and cannot leave 32 bits.
+ pub fn is_identity(&self) -> bool {
+ self.words.m == IDENTITY
+ }
+
+ /// Whether the gate acts inside tiles of `size` amplitudes: diagonal, or
+ /// its target's bit below `size`.
+ fn fits_tile(&self, size: u32) -> bool {
+ self.diag || self.words.tbit < size
+ }
+}
+
/// One step of a program.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Step {
/// `gates[gate]` over the whole state: one dispatch.
Gate { gate: u32 },
+ /// `gates[first..first + count]`, in order, on each tile of `size`
+ /// consecutive amplitudes (a power of two): one dispatch.
+ Tile { first: u32, count: u32, size: u32 },
}
/// What a member runs for one circuit from one basis state.
@@ -49,24 +158,60 @@ pub struct Program {
/// Validates exactly as [`Circuit::simulate_from`] does, with the same errors
/// in the same order (`validate()`, then [`QuantumError::TooManyQubits`]),
-/// then plans one [`Step::Gate`] per op, in op-log order.
+/// then plans one [`Step::Gate`] per op, in op-log order:
+/// [`compile_with`]`(c, start, &Config::PER_GATE)`.
pub fn compile(c: &Circuit, start: usize) -> Result {
+ compile_with(c, start, &Config::PER_GATE)
+}
+
+/// Validates as [`compile`] does, then plans the ops under `cfg`
+/// ([`schedule`]).
+///
+/// Panics if `cfg.tile_bits` is above [`TILE_BITS_MAX`] or `cfg.run_max` is
+/// zero.
+pub fn compile_with(c: &Circuit, start: usize, cfg: &Config) -> Result {
c.validate()?;
if c.n_qubits > MAX_QUBITS {
return Err(QuantumError::TooManyQubits(c.n_qubits));
}
- let gates: Vec = c
- .ops
- .iter()
- .enumerate()
- .map(|(k, g)| {
- let words = lanes::gate_words(g, c.gateset);
- // `validate` holds the op count to `u32`.
- PlannedGate { words, index: k as u32, diag: words.m[2..6] == [0; 4] }
- })
- .collect();
- let steps = (0..gates.len() as u32).map(|gate| Step::Gate { gate }).collect();
- Ok(Program { n_qubits: c.n_qubits, start: start % (1usize << c.n_qubits), gates, steps })
+ // `validate` holds the op count to `u32`.
+ let gates = c.ops.iter().enumerate().map(|(k, g)| PlannedGate::new(lanes::gate_words(g, c.gateset), k as u32)).collect();
+ Ok(schedule(c.n_qubits, start, gates, cfg))
+}
+
+/// Plans `gates`, in op-log order, on a state of `n_qubits` from the basis
+/// state `start % 2^n`: with `cfg.skip_identity`, every gate whose words are
+/// exactly the identity's (and whose target and controls fit the state) is
+/// left out; then each maximal run of consecutive gates that act inside a
+/// tile of [`Config::tile_size`] amplitudes, split every [`Config::run_cap`]
+/// gates, is a [`Step::Tile`], and every other gate a [`Step::Gate`].
+///
+/// Panics if `n_qubits` is above [`MAX_QUBITS`], `cfg.tile_bits` above
+/// [`TILE_BITS_MAX`], or `cfg.run_max` zero.
+pub fn schedule(n_qubits: u8, start: usize, gates: Vec, cfg: &Config) -> Program {
+ assert!(n_qubits <= MAX_QUBITS, "a program holds at most {MAX_QUBITS} qubits, not {n_qubits}");
+ assert!(cfg.tile_bits <= TILE_BITS_MAX, "tiles hold at most 2^{TILE_BITS_MAX} amplitudes, not 2^{}", cfg.tile_bits);
+ assert!(cfg.run_max >= 1, "a tile run holds at least one gate");
+ // An identity whose target or controls a kernel refuses stays, to be refused.
+ let len = 1usize << n_qubits;
+ let gates: Vec = if cfg.skip_identity { gates.into_iter().filter(|g| !(g.is_identity() && tile_shape(&g.words, len))).collect() } else { gates };
+ let (size, cap) = (cfg.tile_size(n_qubits), cfg.run_cap(n_qubits) as usize);
+ let mut steps = Vec::new();
+ let mut k = 0;
+ while k < gates.len() {
+ if size != 0 && gates[k].fits_tile(size) {
+ let first = k;
+ while k < gates.len() && k - first < cap && gates[k].fits_tile(size) {
+ k += 1;
+ }
+ // `validate` holds the op count, and so `first` and `k`, to `u32`.
+ steps.push(Step::Tile { first: first as u32, count: (k - first) as u32, size });
+ } else {
+ steps.push(Step::Gate { gate: k as u32 });
+ k += 1;
+ }
+ }
+ Program { n_qubits, start: start % len, gates, steps }
}
/// A run in lanes.
@@ -79,32 +224,119 @@ pub struct LaneRun {
pub out_of_range: Option,
}
-/// The program on the CPU: `amps[start] = (2^30, 0)`, then each step with
-/// [`lanes::apply_words`]. Panics only if the program is malformed, which
-/// [`compile`] never makes.
+/// The program on the CPU: `amps[start] = (2^30, 0)`, then each step as
+/// [`execute_from`] runs it. Panics only if the program is malformed, which
+/// [`compile`], [`compile_with`] and [`schedule`] never make.
pub fn execute(p: &Program) -> LaneRun {
let mut amps = vec![[0i32; 2]; 1usize << p.n_qubits];
amps[p.start] = [ONE as i32, 0];
+ execute_from(p, &s).expect("a planned program's words fit its state")
+}
+
+/// The program's steps on the CPU from `state` (which replaces the basis
+/// state `p.start`): each [`Step::Gate`] with [`lanes::apply_words`], each
+/// [`Step::Tile`] tile by tile with a tile kernel's own index arithmetic.
+/// `None` if `state` is not `2^n` lanes or the program is malformed: a step
+/// naming gates it does not hold, a tile size that is not a power of two of
+/// at most `2^TILE_BITS_MAX` and the state's length, an empty run, or words
+/// a kernel refuses (those [`lanes::apply_words`] refuses, and in a run a
+/// gate that neither is diagonal nor has its target's bit below the tile's
+/// size).
+pub fn execute_from(p: &Program, state: &[Lane]) -> Option {
+ if p.n_qubits > MAX_QUBITS || state.len() != 1usize << p.n_qubits {
+ return None;
+ }
+ let mut amps = state.to_vec();
let mut out_of_range: Option = None;
+ let mut flag = |index: u32| out_of_range = Some(out_of_range.map_or(index, |k| k.min(index)));
for step in &p.steps {
match *step {
Step::Gate { gate } => {
- let g = &p.gates[gate as usize];
- let fits = lanes::apply_words(&mut amps, &g.words).expect("a compiled program's words fit its state");
- if !fits {
- out_of_range = Some(out_of_range.map_or(g.index, |k| k.min(g.index)));
+ let g = p.gates.get(gate as usize)?;
+ if !lanes::apply_words(&mut amps, &g.words)? {
+ flag(g.index);
+ }
+ }
+ Step::Tile { first, count, size } => {
+ let run = p.gates.get(first as usize..(first as usize).checked_add(count as usize)?)?;
+ let size = size as usize;
+ if run.is_empty() || !size.is_power_of_two() || size > 1 << TILE_BITS_MAX || size > amps.len() {
+ return None;
+ }
+ for g in run {
+ if !tile_shape(&g.words, amps.len()) || !(g.diag || (g.words.tbit as usize) < size) {
+ return None;
+ }
+ }
+ for base in (0..amps.len()).step_by(size) {
+ for g in run {
+ if !tile_gate(&mut amps[base..base + size], base, g) {
+ flag(g.index);
+ }
+ }
}
}
}
}
- LaneRun { amps, out_of_range }
+ Some(LaneRun { amps, out_of_range })
+}
+
+/// The predicate both kernels refuse a record by, for a state of `len` lanes:
+/// `tbit` a power of two below `len`, `ctrl` clear of `tbit` and of every bit
+/// at or above `len`.
+fn tile_shape(w: &GateWords, len: usize) -> bool {
+ let (tbit, ctrl) = (w.tbit as usize, w.ctrl as usize);
+ tbit.is_power_of_two() && tbit < len && ctrl & tbit == 0 && ctrl & !(len - 1) == 0
+}
+
+/// The low 32 bits of an exact value, and whether it is a 32-bit one.
+fn lane(v: [i64; 2]) -> (Lane, bool) {
+ // `as` keeps the low 32 bits.
+ ([v[0] as i32, v[1] as i32], i32::try_from(v[0]).is_ok() && i32::try_from(v[1]).is_ok())
+}
+
+/// One gate of a run on the tile starting at amplitude `base`, as a tile
+/// kernel computes it: a diagonal gate per amplitude, row 0 with a zero
+/// partner where the target's bit is clear and row 1 where it is set; any
+/// other gate per pair, the pair `q` being `q` with a zero inserted at the
+/// target's bit and that index with the bit set. The control test reads the
+/// state-wide index. Returns whether every exact result fits in 32 bits.
+fn tile_gate(tile: &mut [Lane], base: usize, g: &PlannedGate) -> bool {
+ let (t, c) = (g.words.tbit as usize, g.words.ctrl as usize);
+ let mut fits = true;
+ if g.diag {
+ for (e, a) in tile.iter_mut().enumerate() {
+ let gi = base + e;
+ if gi & c == c {
+ let row = if gi & t == 0 { lanes::exact_row(&g.words, 0, *a, [0, 0]) } else { lanes::exact_row(&g.words, 1, [0, 0], *a) };
+ let (v, ok) = lane(row);
+ *a = v;
+ fits &= ok;
+ }
+ }
+ } else {
+ for q in 0..tile.len() / 2 {
+ let low = q & (t - 1);
+ let li = ((q ^ low) << 1) | low;
+ let lj = li | t;
+ if (base + li) & c == c {
+ let (a0, a1) = (tile[li], tile[lj]);
+ let (v0, ok0) = lane(lanes::exact_row(&g.words, 0, a0, a1));
+ let (v1, ok1) = lane(lanes::exact_row(&g.words, 1, a0, a1));
+ (tile[li], tile[lj]) = (v0, v1);
+ fits &= ok0 && ok1;
+ }
+ }
+ }
+ fits
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum::{BaseGate, Gate, Gateset};
- use crate::quantum_fabric::{Corpus, FABRIC_DEFAULT};
+ use crate::quantum_fabric::tests::configurations;
+ use crate::quantum_fabric::{Certification, Corpus, FABRIC_DEFAULT, Parity, apply_kernel, wgsl};
fn widened(lanes: &[Lane]) -> Vec<[i64; 2]> {
lanes.iter().map(|a| [i64::from(a[0]), i64::from(a[1])]).collect()
@@ -114,6 +346,18 @@ mod tests {
c.simulate_from(start).unwrap().amps.iter().map(|a| [a.re, a.im]).collect()
}
+ 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)
+ }
+
#[test]
fn execute_is_the_native_simulator() {
for gs in Gateset::ALL {
@@ -141,17 +385,23 @@ mod tests {
assert_eq!(p.steps, (0..6).map(|gate| Step::Gate { gate }).collect::>());
assert_eq!(p.gates.iter().map(|g| g.index).collect::>(), [0, 1, 2, 3, 4, 5]);
assert_eq!(p.gates.iter().map(|g| g.diag).collect::>(), [false, true, false, true, false, true]);
+ assert_eq!(compile_with(&c, 11, &Config::PER_GATE).unwrap(), p);
// No qubits: no gates, and the one amplitude.
let empty = compile(&Circuit::with_gateset(0, Gateset::V2), 9).unwrap();
assert_eq!((empty.start, empty.gates.len()), (0, 0));
assert_eq!(execute(&empty), LaneRun { amps: vec![[ONE as i32, 0]], out_of_range: None });
- // It refuses what the native simulator refuses, with the same error.
- for c in crate::quantum_fabric::tests::refused_circuits() {
- assert_eq!(compile(&c, 0).unwrap_err(), c.simulate_from(0).unwrap_err(), "{c:?}");
+ assert_eq!(compile_with(&Circuit::with_gateset(0, Gateset::V2), 9, &Config::TILED).unwrap(), empty);
+ // It refuses what the native simulator refuses, with the same error,
+ // in every configuration.
+ for cfg in configurations() {
+ for c in crate::quantum_fabric::tests::refused_circuits() {
+ assert_eq!(compile_with(&c, 0, &cfg).unwrap_err(), c.simulate_from(0).unwrap_err(), "{c:?}");
+ }
}
let mut wide = Circuit::new(MAX_QUBITS + 1);
wide.ops.push(Gate { base: BaseGate::X, controls: vec![], target: 0, param: 0 });
assert_eq!(compile(&wide, 0).unwrap_err(), QuantumError::TooManyQubits(MAX_QUBITS + 1));
+ assert_eq!(compile_with(&wide, 0, &Config::TILED).unwrap_err(), QuantumError::TooManyQubits(MAX_QUBITS + 1));
assert_eq!(compile(&Circuit::new(MAX_QUBITS + 1), 0).unwrap_err(), QuantumError::TooManyQubits(MAX_QUBITS + 1));
// Validation comes first, as in the native simulator.
let mut both = Circuit::new(MAX_QUBITS + 1);
@@ -159,4 +409,253 @@ mod tests {
assert_eq!(compile(&both, 0).unwrap_err(), both.simulate_from(0).unwrap_err());
assert!(matches!(compile(&both, 0), Err(QuantumError::Invalid(_))));
}
+
+ /// The lanes32 digests, random-v1 and fabric-v1, per gateset.
+ const PINS: [(Gateset, &str, &str); 2] = [
+ (Gateset::V1, "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36", "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7"),
+ (Gateset::V2, "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2", "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809"),
+ ];
+
+ /// Every configuration's program equals the native simulator over
+ /// random-v1 (300 circuits) and fabric-v1, both gatesets, and certifies
+ /// with the lanes32 pins: a certificate of one configuration
+ /// (implementation `lanes32-plan`) has the pinned corpus digest.
+ #[test]
+ fn every_configuration_is_the_native_simulator() {
+ let configs = configurations();
+ assert_eq!(configs.len(), 10);
+ for (gs, random_pin, fabric_pin) in PINS {
+ // fabric-v1 is random-v1's first 200 circuits, then the wide ones.
+ let fabric = FABRIC_DEFAULT.circuits(gs);
+ let more = Corpus::Random { seed: 7, cases: 300 }.circuits(gs);
+ assert_eq!(fabric[..200], more[..200]);
+ let circuits: Vec<&Circuit> = fabric.iter().chain(&more[200..]).collect();
+ let reference: Vec<_> = circuits.iter().map(|c| lanes::reference_output(c).unwrap()).collect();
+ for cfg in &configs {
+ let (mut random_v1, mut fabric_v1) = (Certification::new(), Certification::new());
+ for (k, (c, want)) in circuits.iter().zip(&reference).enumerate() {
+ let run = execute(&compile_with(c, 0, cfg).unwrap());
+ assert_eq!(run.out_of_range, None, "{cfg:?} {gs:?} case {k}");
+ let out = lanes::output(&run.amps);
+ assert_eq!(&out, want, "{cfg:?} {gs:?} case {k}");
+ let input = lanes::circuit_bytes(c);
+ if k < 200 {
+ random_v1.add(&input, want, &out);
+ }
+ if k < fabric.len() {
+ fabric_v1.add(&input, want, &out);
+ }
+ }
+ // A start other than zero, on random-v1's first circuits.
+ for c in &circuits[..40] {
+ assert_eq!(widened(&execute(&compile_with(c, 5, cfg).unwrap()).amps), native(c, 5), "{cfg:?} {gs:?} start 5");
+ }
+ let random_v1 = random_v1.finish(apply_kernel(gs), "lanes32-plan", "local/test", &Corpus::Random { seed: 7, cases: 200 }.generator(gs));
+ let fabric_v1 = fabric_v1.finish(apply_kernel(gs), "lanes32-plan", "local/test", &FABRIC_DEFAULT.generator(gs));
+ for (cert, cases, pin) in [(random_v1, 200, random_pin), (fabric_v1, 224, fabric_pin)] {
+ assert_eq!((cert.parity, cert.cases), (Parity::BitExact, cases), "{cfg:?} {gs:?}");
+ assert_eq!(hex(&cert.corpus), pin, "{cfg:?} {gs:?}");
+ }
+ }
+ }
+ }
+
+ /// A random circuit of `ops` gates on `n` qubits: 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
+ }
+
+ /// Long circuits: the tiled program equals the per-gate one and the
+ /// native simulator, with runs long enough to be split at their cap.
+ #[test]
+ fn long_circuits_tile_as_they_run_gate_by_gate() {
+ let short_runs = Config { tile_bits: 3, run_max: 7, skip_identity: true };
+ for gs in Gateset::ALL {
+ for (n, ops, seed) in [(4u8, 3000usize, 0x6c6f_6e67u64), (11, 600, 0x7469_6c65), (12, 400, 0x7275_6e73)] {
+ let c = random_circuit(n, ops, seed + n as u64, gs);
+ assert!(c.ops.len() as u64 <= lanes::horizon(n, gs));
+ let per_gate = execute(&compile(&c, 0).unwrap());
+ assert_eq!((per_gate.out_of_range, widened(&per_gate.amps)), (None, native(&c, 0)), "{gs:?} n = {n}");
+ for cfg in [Config::TILED, short_runs] {
+ let p = compile_with(&c, 0, &cfg).unwrap();
+ let longest = p.steps.iter().filter_map(|s| match *s { Step::Tile { count, .. } => Some(count), _ => None }).max().unwrap();
+ assert!(longest > 1, "{cfg:?} {gs:?} n = {n}");
+ assert!(longest <= cfg.run_cap(n));
+ assert_eq!(execute(&p), per_gate, "{cfg:?} {gs:?} n = {n}");
+ }
+ }
+ // A 4-qubit circuit is one tile, and every gate fits it: the
+ // runs are cut only at the cap.
+ let c = random_circuit(4, 3000, 3, gs);
+ let p = compile_with(&c, 0, &Config::TILED).unwrap();
+ let counts: Vec = p.steps.iter().map(|s| match *s { Step::Tile { count, size, .. } => { assert_eq!(size, 16); count } other => panic!("{other:?}") }).collect();
+ assert_eq!(counts.iter().sum::() as usize, p.gates.len());
+ assert!(counts[..counts.len() - 1].iter().all(|&k| k == 1024), "{counts:?}");
+ }
+ }
+
+ /// Any words, whatever their range: the tiled program equals the gates
+ /// one by one ([`lanes::run_words`]), lanes and report, with diagonal
+ /// gates on every target, controls above and below the tile's bits, and
+ /// runs cut by gates that do not fit a tile.
+ #[test]
+ fn tiles_are_exact_on_any_words() {
+ let mut s = 0x7469_6c65_7321u64;
+ let (mut flagged, mut clean, mut tiled) = (0, 0, 0);
+ for case in 0..1500 {
+ let n = 1 + (next(&mut s) % 12) as u8;
+ let full = case % 2 == 0;
+ let value = |s: &mut u64| -> i32 { if full { next(s) as i32 } else { (next(s) % (1 << 23)) as i32 - (1 << 22) } };
+ let state: Vec = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
+ let gates: Vec = (0..1 + next(&mut s) % 24)
+ .map(|_| {
+ let t = (next(&mut s) % u64::from(n)) as u32;
+ let ctrl = (0..u32::from(n)).filter(|&b| b != t && next(&mut s) % 4 == 0).fold(0u32, |acc, b| acc | 1 << b);
+ let mut m = [0u32; 8];
+ let entry = |s: &mut u64| -> u32 { if full { next(s) as u32 } else { ((next(s) % (1 << 30)) as i64 - (1 << 29)) as i32 as u32 } };
+ m.iter_mut().for_each(|x| *x = entry(&mut s));
+ match next(&mut s) % 4 {
+ 0 | 1 => m[2..6].fill(0),
+ 2 => m = IDENTITY,
+ _ => {}
+ }
+ GateWords { m, tbit: 1 << t, ctrl }
+ })
+ .collect();
+ let want = lanes::run_words(&state, &gates).expect("valid shapes");
+ let planned: Vec = gates.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, k as u32)).collect();
+ for cfg in [Config::TILED, Config { tile_bits: 2, run_max: 3, skip_identity: false }] {
+ let p = schedule(n, 0, planned.clone(), &cfg);
+ if p.steps.iter().any(|s| matches!(s, Step::Tile { count, .. } if *count > 1)) {
+ tiled += 1;
+ }
+ assert_eq!(execute_from(&p, &state), Some(want.clone()), "case {case}, n = {n}, {cfg:?}");
+ }
+ if want.out_of_range.is_some() {
+ flagged += 1;
+ } else {
+ clean += 1;
+ }
+ }
+ assert!(flagged >= 100 && clean >= 100 && tiled >= 1000, "{flagged} flagged, {clean} clean, {tiled} with a run of two gates or more");
+ }
+
+ #[test]
+ fn tiles_cover_every_path() {
+ for gs in Gateset::ALL {
+ let mut count = std::collections::BTreeMap::<&str, usize>::new();
+ let mut add = |what: &'static str, k: usize| *count.entry(what).or_default() += k;
+ for c in FABRIC_DEFAULT.circuits(gs) {
+ let p = compile_with(&c, 0, &Config::TILED).unwrap();
+ add("identity skipped", c.ops.len() - p.gates.len());
+ for step in &p.steps {
+ match *step {
+ Step::Gate { .. } => add("per-gate step", 1),
+ Step::Tile { first, count, size } => {
+ add("tile run of two gates or more", usize::from(count > 1));
+ add("tile of 1024 amplitudes", usize::from(size == 1024));
+ add("tile below 1024 amplitudes", usize::from(size < 1024));
+ let tiles = (1u32 << c.n_qubits) / size;
+ add("tile grid of more than one row", usize::from(wgsl::tile_grid(tiles, wgsl::TILE_X_GROUPS_MAX).y > 1));
+ for g in &p.gates[first as usize..(first + count) as usize] {
+ add("diagonal gate on a target outside the tile", usize::from(g.diag && g.words.tbit >= size));
+ add("non-diagonal gate in a run", usize::from(!g.diag));
+ add("control above the tile's bits", usize::from(g.words.ctrl >= size));
+ add("control inside the tile", usize::from(g.words.ctrl & (size - 1) != 0));
+ }
+ }
+ }
+ }
+ }
+ assert_eq!(count.len(), 10, "{gs:?}: {count:?}");
+ for (what, k) in &count {
+ assert!(*k > 0, "{gs:?}: no {what}");
+ }
+ }
+ // The cap: 1024 gates per run up to 20 qubits, then half as many for
+ // every qubit more, 16 at 26.
+ for (n, cap) in [(1, 1024), (20, 1024), (21, 512), (24, 64), (26, 16)] {
+ assert_eq!(Config::TILED.run_cap(n), cap, "n = {n}");
+ }
+ assert_eq!((Config::TILED.tile_size(0), Config::TILED.tile_size(3), Config::TILED.tile_size(10), Config::TILED.tile_size(26)), (0, 8, 1024, 1024));
+ assert_eq!((Config::PER_GATE.tile_size(12), Config::PER_GATE.run_cap(12)), (0, 1));
+ }
+
+ #[test]
+ fn the_v1_tail_phases_are_not_skipped() {
+ // P(19..=32) of circuit/1 is (2^30 − 1, 0): not the identity.
+ let p19 = lanes::gate_words(&Gate { base: BaseGate::P, controls: vec![], target: 0, param: 19 }, Gateset::V1);
+ assert_eq!(p19.m, [ONE as u32, 0, 0, 0, 0, 0, (ONE - 1) as u32, 0]);
+ assert!(!PlannedGate::new(p19, 0).is_identity() && PlannedGate::new(p19, 0).diag);
+ let mut c = Circuit::with_gateset(1, Gateset::V1);
+ c.h(0).p(19, 0);
+ let p = compile_with(&c, 0, &Config::TILED).unwrap();
+ assert_eq!(p.gates.len(), 2);
+ assert_eq!(p.steps, [Step::Tile { first: 0, count: 2, size: 2 }]);
+ let run = execute(&p);
+ assert_eq!(widened(&run.amps), native(&c, 0));
+ let h = Gateset::V1.inv_sqrt2() as i32;
+ assert_eq!(run.amps, [[h, 0], [h - 1, 0]]);
+ // It takes 2^29 + 1 to 2^29.
+ let tail = schedule(1, 0, vec![PlannedGate::new(p19, 0)], &Config::TILED);
+ assert_eq!(tail.steps, [Step::Tile { first: 0, count: 1, size: 2 }]);
+ let run = execute_from(&tail, &[[0, 0], [(1 << 29) + 1, 0]]).unwrap();
+ assert_eq!(run.amps, [[0, 0], [1 << 29, 0]]);
+ // The identity itself is skipped, and an identity left in changes
+ // nothing.
+ let mut c = Circuit::with_gateset(2, Gateset::V1);
+ c.h(0);
+ c.ops.push(Gate { base: BaseGate::I, controls: vec![1], target: 0, param: 0 });
+ c.h(1);
+ let skipped = compile_with(&c, 0, &Config::TILED).unwrap();
+ assert_eq!(skipped.gates.iter().map(|g| g.index).collect::>(), [0, 2]);
+ let kept = compile_with(&c, 0, &Config { skip_identity: false, ..Config::TILED }).unwrap();
+ assert_eq!(kept.gates.len(), 3);
+ assert_eq!(execute(&skipped), execute(&kept));
+ }
+
+ /// Programs a tile kernel refuses are refused here too.
+ #[test]
+ fn malformed_programs_are_refused() {
+ let x2 = PlannedGate::new(lanes::gate_words(&Gate { base: BaseGate::X, controls: vec![], target: 2, param: 0 }, Gateset::V2), 0);
+ let z0 = PlannedGate::new(lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2), 0);
+ let state = vec![[0, 0]; 8];
+ let program = |gates: Vec, steps: Vec| Program { n_qubits: 3, start: 0, gates, steps };
+ // A non-diagonal gate whose target is the tile's size: its pairs leave the tile.
+ assert_eq!(execute_from(&program(vec![x2], vec![Step::Tile { first: 0, count: 1, size: 4 }]), &state), None);
+ assert!(execute_from(&program(vec![x2], vec![Step::Tile { first: 0, count: 1, size: 8 }]), &state).is_some());
+ // Sizes and ranges a run cannot have.
+ for (first, count, size) in [(0, 0, 4), (0, 2, 4), (1, 1, 4), (0, 1, 3), (0, 1, 16), (0, 1, 0)] {
+ assert_eq!(execute_from(&program(vec![z0], vec![Step::Tile { first, count, size }]), &state), None, "{first} {count} {size}");
+ }
+ assert_eq!(execute_from(&program(vec![z0], vec![Step::Gate { gate: 1 }]), &state), None);
+ // Words a kernel refuses, inside a run.
+ for w in [GateWords { tbit: 3, ..z0.words }, GateWords { tbit: 8, ..z0.words }, GateWords { ctrl: 1, ..z0.words }, GateWords { ctrl: 8, ..z0.words }] {
+ assert_eq!(execute_from(&program(vec![PlannedGate { words: w, ..z0 }], vec![Step::Tile { first: 0, count: 1, size: 8 }]), &state), None, "{w:?}");
+ }
+ // An identity a kernel would refuse is not skipped, so it is refused.
+ let refused_identity = PlannedGate::new(GateWords { m: IDENTITY, tbit: 3, ctrl: 0 }, 0);
+ let p = schedule(3, 0, vec![refused_identity], &Config::TILED);
+ assert_eq!(p.gates.len(), 1);
+ assert_eq!(execute_from(&p, &state), None);
+ // A state of the wrong length.
+ assert_eq!(execute_from(&program(vec![z0], vec![Step::Gate { gate: 0 }]), &state[..4]), None);
+ }
}
diff --git a/wai/wai-quantum/src/quantum_fabric/wgsl.rs b/wai/wai-quantum/src/quantum_fabric/wgsl.rs
index 1a4a5584f..7b6da73ff 100644
--- a/wai/wai-quantum/src/quantum_fabric/wgsl.rs
+++ b/wai/wai-quantum/src/quantum_fabric/wgsl.rs
@@ -90,14 +90,64 @@
//! to. The rules are checked on the translator's own reading of the text by
//! the unpublished crate `wai/tools/quantum-wgsl-check`.
//!
+//! # Tile runs: a second implementation
+//!
+//! [`TILE_SHADER`] is [`SHADER`] followed by `lanes32_tile.wgsl`, whose entry
+//! point `apply_tile` runs a [`Step::Tile`](super::plan::Step::Tile) of a
+//! program planned with [`Config::TILED`](super::plan::Config::TILED): one
+//! workgroup of [`TILE_WORKGROUP`] invocations per tile of up to 1024
+//! consecutive amplitudes, which it alone touches for the whole dispatch while
+//! it applies every gate of the run to the tile in place, in op-log order. A
+//! run of gates is one dispatch instead of one per gate. The steps of a tiled
+//! program that are single gates run on the same module's `apply`. It is its
+//! own implementation ([`tile_implementation_id`]), certified on its own; the
+//! per-gate kernel stays the member's default and its fallback.
+//!
+//! - **The same function.** `apply_tile` computes every output with `row`, the
+//! per-gate kernel's exact 64-bit row, and stores its low words: for a pair,
+//! the per-gate kernel's computation; for a diagonal gate, the row with a
+//! zero partner, whose exact value is the per-gate one because `fx64(0, x)`
+//! is exactly 0 for every `x`. A gate is flagged when any exact output does
+//! not fit, and the report keeps the lowest op index flagged over every
+//! tile and gate. The tiled program on the CPU ([`plan::execute`]) mirrors
+//! this index arithmetic tile by tile, and equals the per-gate program on
+//! any words ([`plan`](super::plan) documents why it may).
+//! - **No race.** Within a gate each amplitude (diagonal) or pair (otherwise)
+//! belongs to one invocation; a storage barrier (workgroup execution scope,
+//! storage memory) separates each gate from the next, so a gate reads what
+//! the gate before wrote; tiles are disjoint, one workgroup each, and the
+//! dispatch ends before the next step reads the state. A tile outside the
+//! state is reported (bit 2) and not touched; a record outside the gate
+//! buffer reads as malformed, never out of bounds.
+//! - **A uniform barrier.** The early returns depend only on the run record (a
+//! uniform buffer) and the workgroup id. The barrier sits at the top level
+//! of the loop over the run's gates, whose bound is in the uniform record;
+//! that loop holds no `break`, `continue` or `return`, and no branch that
+//! depends on a gate or an invocation holds a barrier. Naga does not check
+//! barrier uniformity; a browser's WGSL compiler, which does, compiles this
+//! text in the browser leg of the member's checks.
+//! - **The size travels in a uniform.** The tile size, the gate range and the
+//! grid are fields of the run record, never substituted into the text, so
+//! the implementation id names the exact text every device compiles.
+//!
+//! The tile file keeps the per-gate rules (unsigned 32-bit values only, literal
+//! shift amounts, no conversion, division or float, no workgroup memory) and
+//! adds exactly one barrier, `storageBarrier()`. It holds no workgroup memory
+//! because the SPIR-V the native translator writes for a workgroup array or
+//! structure carries explicit layout decorations, which a Vulkan SPIR-V
+//! validator rejects for the workgroup storage class; the tile stays in the
+//! state's own planes instead. A device must be caught running each of
+//! [`TILE_MUTANTS`].
+//!
//! # What the argument does not cover
//!
//! Whether a device and its shader compiler conform is not proved here. A
//! member earns that per adapter: by reproducing the pinned corpus digests of
//! [`FABRIC_DEFAULT`](super::FABRIC_DEFAULT), and by being caught running each
-//! of [`MUTANTS`]. Any edit to [`SHADER`] or to the host layout is a new
-//! implementation, with a new [`implementation_id`], whose certificates must be
-//! earned again.
+//! of [`MUTANTS`] (and, for tile runs, [`TILE_MUTANTS`]). Any edit to
+//! [`SHADER`], to the tile file, to the host layout or to the meaning of a
+//! program is a new implementation, with a new [`implementation_id`] or
+//! [`tile_implementation_id`], whose certificates must be earned again.
//!
//! [`lanes`]: super::lanes
//! [`lanes::apply`]: super::lanes::apply
@@ -105,7 +155,8 @@
//! [`lanes::horizon`]: super::lanes::horizon
//! [`plan::execute`]: super::plan::execute
-use super::lanes::GateWords;
+use super::lanes::{GateWords, Lane};
+use super::plan::{PLAN_REVISION, PlannedGate, Program, Step};
use super::{FabricClass, put};
use crate::quantum::MAX_QUBITS;
@@ -116,8 +167,9 @@ pub const SHADER: &str = include_str!("lanes32.wgsl");
pub const IMPLEMENTATION: &str = "wgsl-lanes32";
/// Version of the host-side layout: planar `u32` planes, the 64-byte gate
-/// record, the grid rule. Bumped on any change to them; it is part of the
-/// implementation id.
+/// record, the grid rule, and for tile runs the 48-byte tile gate record, the
+/// 32-byte run record and the tile grid rule. Bumped on any change to them;
+/// it is part of both implementation ids.
pub const HOST_LAYOUT: u32 = 1;
/// Invocations per workgroup, as the kernel declares.
@@ -192,6 +244,84 @@ pub fn gate_grid(n_qubits: u8) -> Grid {
grid(1 << (n_qubits - 1), X_GROUPS_MAX)
}
+/// The kernel with tile runs, `lanes32.wgsl` followed by `lanes32_tile.wgsl`,
+/// exactly as it is compiled: the entry points `apply` and `probe` of
+/// [`SHADER`], and `apply_tile`.
+pub const TILE_SHADER: &str = concat!(include_str!("lanes32.wgsl"), include_str!("lanes32_tile.wgsl"));
+
+/// The tile implementation's name, which its id starts with.
+pub const TILE_IMPLEMENTATION: &str = "wgsl-lanes32-tile";
+
+/// Invocations per workgroup of `apply_tile`, as it declares: a tile of up to
+/// 1024 amplitudes is four per invocation, or two pairs.
+pub const TILE_WORKGROUP: u32 = 256;
+
+/// Workgroups along x of a tile step, at most: at 1024 amplitudes a tile, two
+/// rows or more from 14 qubits on, inside the certification corpus.
+pub const TILE_X_GROUPS_MAX: u32 = 8;
+
+/// Bytes of one tile gate record, the tile kernel's `TileGate`.
+pub const TILE_GATE_RECORD_BYTES: usize = 48;
+
+/// Bytes of one run record, the tile kernel's `TileRun` uniform.
+pub const TILE_RUN_RECORD_BYTES: usize = 32;
+
+/// `"wgsl-lanes32-tile/<16 hex>"`: the first 8 bytes of
+/// BLAKE3(`"wai:quantum-fabric-wgsl\x01"` ‖ put([`TILE_SHADER`]) ‖
+/// [`HOST_LAYOUT`] ‖ [`PLAN_REVISION`], both big-endian `u32`), in hex. The
+/// tile size is a field of the run record, never part of the text, so this
+/// names the exact text every device compiles.
+pub fn tile_implementation_id() -> String {
+ let mut o = DOMAIN_IMPLEMENTATION.to_vec();
+ put(&mut o, TILE_SHADER.as_bytes());
+ o.extend_from_slice(&HOST_LAYOUT.to_be_bytes());
+ o.extend_from_slice(&PLAN_REVISION.to_be_bytes());
+ format!("{TILE_IMPLEMENTATION}/{}", hex(&blake3::hash(&o).as_bytes()[..8]))
+}
+
+/// The 48-byte little-endian storage record of one gate of a tile run:
+/// `m[0..8]`, `tbit`, `ctrl`, `diag` (1 or 0), `index`.
+pub fn tile_gate_record(g: &PlannedGate) -> [u8; TILE_GATE_RECORD_BYTES] {
+ let mut r = [0u8; TILE_GATE_RECORD_BYTES];
+ let w = &g.words;
+ let words = w.m.into_iter().chain([w.tbit, w.ctrl, u32::from(g.diag), g.index]);
+ for (k, v) in words.enumerate() {
+ r[4 * k..4 * k + 4].copy_from_slice(&v.to_le_bytes());
+ }
+ r
+}
+
+/// The 32-byte little-endian uniform record of one tile run: `first` (its
+/// first record in the tile gate buffer), `count`, `size`, `x_groups`,
+/// `tiles`, then three zero words.
+pub fn tile_run_record(first: u32, count: u32, size: u32, x_groups: u32, tiles: u32) -> [u8; TILE_RUN_RECORD_BYTES] {
+ let mut r = [0u8; TILE_RUN_RECORD_BYTES];
+ for (k, v) in [first, count, size, x_groups, tiles, 0, 0, 0].into_iter().enumerate() {
+ r[4 * k..4 * k + 4].copy_from_slice(&v.to_le_bytes());
+ }
+ r
+}
+
+/// A tile step's dispatch: `x` by `y` workgroups, where the workgroup at
+/// `(wx, wy)` holds tile `wy·x + wx` if it is below `tiles`.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct TileGrid {
+ pub x: u32,
+ pub y: u32,
+ pub tiles: u32,
+}
+
+/// Workgroups covering `tiles` tiles: `x = min(tiles, max_x)`,
+/// `y = ⌈tiles / x⌉`. In production `max_x` is [`TILE_X_GROUPS_MAX`]; both
+/// are powers of two, so `y = tiles / x` exactly.
+///
+/// Panics if `tiles` or `max_x` is zero.
+pub fn tile_grid(tiles: u32, max_x: u32) -> TileGrid {
+ assert!(tiles >= 1 && max_x >= 1, "a tile grid covers at least one tile, with at least one workgroup along x");
+ let x = tiles.min(max_x);
+ TileGrid { x, y: tiles.div_ceil(x), tiles }
+}
+
/// What a WebGPU member is, as its id hashes it. The device module fills it
/// from the adapter's information and the granted limits; the fields are plain
/// values here so that the id is testable without a device.
@@ -272,6 +402,13 @@ pub enum Detector {
Certificate,
/// A result that left 32 bits is not reported.
Range,
+ /// The lanes of [`race_witness`], a tile run in which invocations read
+ /// amplitudes other invocations wrote for the gate before, differ from
+ /// the CPU's.
+ Race,
+ /// [`malformed_tile_run`], a run holding a gate the tile kernel must
+ /// refuse, is not refused as a host defect.
+ Malformed,
}
/// A textual edit of [`SHADER`] that a conforming device must be caught
@@ -310,6 +447,72 @@ pub fn mutate(m: &Mutant) -> String {
SHADER.replacen(m.find, m.replace, 1)
}
+/// The mutants of the tile kernel. Each `find` occurs exactly once in
+/// [`TILE_SHADER`], in its tile part.
+pub const TILE_MUTANTS: [Mutant; 3] = [
+ // The barrier between gates is gone: a gate may read what the gate before has not yet written.
+ Mutant {
+ name: "T1",
+ find: "storageBarrier(); // this gate's writes before the next gate's reads",
+ replace: "// no barrier between gates",
+ detector: Detector::Race,
+ },
+ // The report never keeps the op index of a result that left 32 bits.
+ Mutant { name: "T2", find: "select(bad, h.index, h.index < bad)", replace: "bad", detector: Detector::Range },
+ // A gate whose pairs leave the tile is run instead of refused.
+ Mutant { name: "T3", find: "h.tbit < size", replace: "h.tbit <= size", detector: Detector::Malformed },
+];
+
+/// [`TILE_SHADER`] with `m` applied. Panics unless `m.find` occurs exactly
+/// once.
+pub fn mutate_tile(m: &Mutant) -> String {
+ let n = TILE_SHADER.matches(m.find).count();
+ assert_eq!(n, 1, "mutant {}: `{}` occurs {n} times in the tile kernel", m.name, m.find);
+ TILE_SHADER.replacen(m.find, m.replace, 1)
+}
+
+/// A state of `2^n_qubits` lanes (10 or more qubits, at most 20) and a run of
+/// 1024 gates that a tiled plan makes one tile step per tile, built so that a
+/// tile kernel without a barrier between gates goes wrong: diagonal gates
+/// whose controls (qubits 5, 6 and 7) leave most invocations of a workgroup
+/// idle alternate with gates on qubits 0 to 4, whose pairs are held by other
+/// invocations than the amplitudes the diagonal gates updated. Idle
+/// invocations run ahead into the next gate and read amplitudes not yet
+/// written. Every value spans the whole 32-bit range, so a stale read shows in
+/// the lanes. Panics unless `10 ≤ n_qubits ≤ 20`.
+pub fn race_witness(n_qubits: u8) -> (Vec, Vec) {
+ assert!((10..=20).contains(&n_qubits), "the race witness takes 10 to 20 qubits, not {n_qubits}");
+ let mut s = 0x7261_6365u64 + u64::from(n_qubits);
+ let mut next = || {
+ 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)
+ };
+ let state: Vec = (0..1usize << n_qubits).map(|_| [next() as i32, next() as i32]).collect();
+ let gates = (0..1024u32)
+ .map(|r| {
+ let mut m = [0u32; 8];
+ m.iter_mut().for_each(|x| *x = next() as u32);
+ if r % 2 == 0 {
+ m[2..6].fill(0);
+ GateWords { m, tbit: 1 << (8 + (r / 2) % 2), ctrl: 0b1110_0000 }
+ } else {
+ GateWords { m, tbit: 1 << ((r / 2) % 5), ctrl: 0 }
+ }
+ })
+ .collect();
+ (state, gates)
+}
+
+/// A program a tile kernel must refuse as a host defect: on 3 qubits, a tile
+/// step of 4 amplitudes holding `X` on qubit 2, whose pairs leave the tile.
+pub fn malformed_tile_run() -> Program {
+ let x = GateWords { m: [0, 0, 1 << 30, 0, 1 << 30, 0, 0, 0], tbit: 4, ctrl: 0 };
+ Program { n_qubits: 3, start: 0, gates: vec![PlannedGate::new(x, 0)], steps: vec![Step::Tile { first: 0, count: 1, size: 4 }] }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -642,4 +845,157 @@ mod tests {
let z = lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2);
assert_eq!(lanes::apply_words(&mut [[0, 0], [i32::MIN, 0]], &z), Some(false));
}
+
+ /// Changes only with a reviewed edit to the tile kernel or to the per-gate
+ /// kernel it is appended to, to the argument in this module's
+ /// documentation, to the host layout ([`HOST_LAYOUT`]) or to the meaning
+ /// of a program ([`PLAN_REVISION`]): every certificate of the old id must
+ /// then be earned again.
+ const PINNED_TILE_IMPLEMENTATION: &str = "wgsl-lanes32-tile/de6fb388454a0144";
+
+ #[test]
+ fn tile_implementation_id_is_pinned() {
+ let id = tile_implementation_id();
+ assert_eq!(id, PINNED_TILE_IMPLEMENTATION);
+ let digest = id.strip_prefix("wgsl-lanes32-tile/").expect("the implementation name");
+ assert_eq!(digest.len(), 16);
+ assert!(digest.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)), "{id}");
+ assert_ne!(id.split('/').nth(1), implementation_id().split('/').nth(1));
+ // The tile kernel is appended to the per-gate one, both with LF line
+ // endings, and names no tile size.
+ assert!(TILE_SHADER.starts_with(SHADER) && TILE_SHADER.len() > SHADER.len());
+ assert!(!TILE_SHADER.contains('\r'));
+ assert_eq!(TILE_SHADER.matches("@compute").count(), 3);
+ assert_eq!(TILE_SHADER.matches("storageBarrier()").count(), 1);
+ assert!(!TILE_SHADER.contains("workgroupBarrier") && !TILE_SHADER.contains("var"));
+ assert!(!SHADER.contains("Barrier"));
+ }
+
+ #[test]
+ fn tile_records_layout() {
+ let g = PlannedGate { words: GateWords { m: [1, 2, 3, 4, 5, 6, 7, 8], tbit: 4, ctrl: 3 }, index: 9, diag: true };
+ let r = tile_gate_record(&g);
+ let word = |r: &[u8], k: usize| u32::from_le_bytes(r[4 * k..4 * k + 4].try_into().unwrap());
+ for k in 0..8 {
+ assert_eq!(word(&r, k), k as u32 + 1, "m[{k}]");
+ }
+ assert_eq!((word(&r, 8), word(&r, 9), word(&r, 10), word(&r, 11)), (4, 3, 1, 9));
+ assert_eq!(word(&tile_gate_record(&PlannedGate { diag: false, ..g }), 10), 0);
+ let r = tile_run_record(5, 6, 1024, 8, 16);
+ assert_eq!((0..5).map(|k| word(&r, k)).collect::>(), [5, 6, 1024, 8, 16]);
+ assert_eq!(r[20..32], [0; 12]);
+ // Little-endian: the low byte first.
+ assert_eq!(tile_run_record(0x0102_0304, 1, 2, 1, 1)[0..4], [0x04, 0x03, 0x02, 0x01]);
+ }
+
+ #[test]
+ fn tile_grids_cover_every_tile_once() {
+ for n in 0..=16u32 {
+ let tiles = 1u32 << n;
+ for max_x in [1, 3, TILE_X_GROUPS_MAX] {
+ let g = tile_grid(tiles, max_x);
+ assert!(g.x >= 1 && g.x <= max_x && g.tiles == tiles, "{g:?}");
+ let mut hits = vec![0u32; tiles as usize];
+ for wy in 0..g.y {
+ for wx in 0..g.x {
+ let tile = wy * g.x + wx;
+ if tile < tiles {
+ hits[tile as usize] += 1;
+ }
+ }
+ }
+ assert!(hits.iter().all(|&h| h == 1), "{g:?}");
+ assert!(u64::from(g.x) * u64::from(g.y - 1) < u64::from(tiles), "{g:?}");
+ }
+ // In production both are powers of two: an exact grid.
+ let g = tile_grid(tiles, TILE_X_GROUPS_MAX);
+ assert_eq!(g.x * g.y, tiles);
+ }
+ // 26 qubits in tiles of 1024: (8, 8192).
+ assert_eq!(tile_grid(1 << 16, TILE_X_GROUPS_MAX), TileGrid { x: 8, y: 8192, tiles: 1 << 16 });
+ }
+
+ #[test]
+ fn tile_mutants_apply_once() {
+ let names: Vec<&str> = TILE_MUTANTS.iter().map(|m| m.name).collect();
+ assert_eq!(names, ["T1", "T2", "T3"]);
+ for m in &TILE_MUTANTS {
+ let text = mutate_tile(m);
+ assert_ne!(text, TILE_SHADER, "{}", m.name);
+ // The edit is in the tile part: the per-gate kernel is untouched.
+ assert!(text.starts_with(SHADER), "{}", m.name);
+ assert!(!SHADER.contains(m.find), "{}", m.name);
+ }
+ let detectors: Vec = TILE_MUTANTS.iter().map(|m| m.detector).collect();
+ assert_eq!(detectors, [Detector::Race, Detector::Range, Detector::Malformed]);
+ // T1 leaves no barrier.
+ assert!(!mutate_tile(&TILE_MUTANTS[0]).contains("storageBarrier()"));
+ // The per-gate mutants apply to the tile kernel's copy of the per-gate one as well.
+ for m in &MUTANTS {
+ assert_eq!(TILE_SHADER.matches(m.find).count(), 1, "{}", m.name);
+ }
+ }
+
+ /// A tile run with no barrier between its gates, in one schedule a device
+ /// may choose: each of the [`TILE_WORKGROUP`] invocations runs every gate
+ /// of the run before the next invocation starts. With the barrier, the
+ /// only order is gate by gate.
+ fn run_invocation_by_invocation(tile: &mut [Lane], base: usize, gates: &[PlannedGate]) {
+ let size = tile.len();
+ let lanes_of = |v: [i64; 2]| [v[0] as i32, v[1] as i32];
+ for lid in 0..TILE_WORKGROUP as usize {
+ for g in gates {
+ let (t, c) = (g.words.tbit as usize, g.words.ctrl as usize);
+ if g.diag {
+ for e in (0..4).map(|k| lid + TILE_WORKGROUP as usize * k).filter(|&e| e < size && (base + e) & c == c) {
+ let a = tile[e];
+ tile[e] = lanes_of(if (base + e) & t == 0 { lanes::exact_row(&g.words, 0, a, [0, 0]) } else { lanes::exact_row(&g.words, 1, [0, 0], a) });
+ }
+ } else {
+ for q in (0..2).map(|k| lid + TILE_WORKGROUP as usize * k).filter(|&q| q < size / 2) {
+ let low = q & (t - 1);
+ let li = ((q ^ low) << 1) | low;
+ let lj = li | t;
+ if (base + li) & c == c {
+ let (a0, a1) = (tile[li], tile[lj]);
+ (tile[li], tile[lj]) = (lanes_of(lanes::exact_row(&g.words, 0, a0, a1)), lanes_of(lanes::exact_row(&g.words, 1, a0, a1)));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn tile_mutant_witnesses_exist() {
+ use crate::quantum_fabric::plan::{self, Config};
+ // T1: the race witness is one tile step per tile, gate by gate it is
+ // the CPU's, and invocation by invocation, which a kernel without the
+ // barrier between gates may do, it is not.
+ for n in [10u8, 11] {
+ let (state, words) = race_witness(n);
+ let planned: Vec = words.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, k as u32)).collect();
+ let p = plan::schedule(n, 0, planned.clone(), &Config::TILED);
+ assert_eq!(p.steps, [Step::Tile { first: 0, count: 1024, size: 1024 }], "n = {n}");
+ let want = lanes::run_words(&state, &words).unwrap();
+ assert_eq!(plan::execute_from(&p, &state), Some(want.clone()));
+ let mut racy = state.clone();
+ for base in (0..racy.len()).step_by(1024) {
+ run_invocation_by_invocation(&mut racy[base..base + 1024], base, &planned);
+ }
+ let differing = racy.iter().zip(&want.amps).filter(|(a, b)| a != b).count();
+ assert!(differing > 100, "n = {n}: {differing} lanes differ");
+ }
+ // T2: a result that leaves 32 bits inside a tile step is reported.
+ let z = lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2);
+ let p = plan::schedule(1, 0, vec![PlannedGate::new(z, 0)], &Config::TILED);
+ assert_eq!(p.steps, [Step::Tile { first: 0, count: 1, size: 2 }]);
+ assert_eq!(plan::execute_from(&p, &[[0, 0], [i32::MIN, 0]]).unwrap().out_of_range, Some(0));
+ // T3: the malformed run is refused, and the same gate in a tile that
+ // holds its pairs is not.
+ let bad = malformed_tile_run();
+ assert_eq!(plan::execute_from(&bad, &[[0, 0]; 8]), None);
+ let held = Program { steps: vec![Step::Tile { first: 0, count: 1, size: 8 }], ..bad.clone() };
+ assert!(plan::execute_from(&held, &[[0, 0]; 8]).is_some());
+ }
}
diff --git a/wai/wai-quantum/src/quantum_fabric_webgpu.rs b/wai/wai-quantum/src/quantum_fabric_webgpu.rs
index 361d83a34..de383b475 100644
--- a/wai/wai-quantum/src/quantum_fabric_webgpu.rs
+++ b/wai/wai-quantum/src/quantum_fabric_webgpu.rs
@@ -17,6 +17,13 @@
//! the state comes back in [`READBACK_CHUNK`] pieces. The kernel reports
//! exactly when the reference's result leaves 32 bits; the member then returns
//! [`WebGpuError::OutOfRange`], never a state.
+//! - **Tile runs** ([`WebGpuMember::tiled`]): the same device running the tile
+//! implementation ([`wgsl::tile_implementation_id`]), which plans circuits
+//! with [`Config::TILED`] and runs each run of gates that act inside a tile
+//! as one dispatch, one workgroup per tile; its run records go in a second
+//! uniform ring and its gates in a storage buffer, per submission. It is
+//! probed and certified on its own; the per-gate member stays the default
+//! and the fallback.
//! - **Certification** ([`WebGpuMember::certify`], [`WebGpuMember::member`]):
//! the member's own run of a corpus, folded case by case into a
//! [`Certification`]. The corpus digest covers inputs and outputs only, so a
@@ -61,7 +68,7 @@
use crate::quantum::{Amp, Circuit, Gateset, MAX_QUBITS, ONE, QuantumError, StateVector};
use crate::quantum_fabric::lanes::{self, GateWords, Lane};
-use crate::quantum_fabric::plan::{self, LaneRun, Program, Step};
+use crate::quantum_fabric::plan::{self, Config, LaneRun, PlannedGate, Program, Step};
use crate::quantum_fabric::wgsl::{self, AdapterIdentity};
use crate::quantum_fabric::{Certificate, Certification, Corpus, FabricClass, KernelEntry, Member, Presence, apply_kernel};
use core::future::Future;
@@ -253,13 +260,15 @@ struct Sink {
lost: Option,
}
-/// The kernel compiled on a device: both entry points, with their layouts.
+/// The kernel compiled on a device: its entry points, with their layouts;
+/// `apply_tile` only for the tile kernel.
#[derive(Clone)]
struct Pipelines {
apply_layout: wgpu::BindGroupLayout,
probe_layout: wgpu::BindGroupLayout,
apply: wgpu::ComputePipeline,
probe: wgpu::ComputePipeline,
+ tile: Option<(wgpu::BindGroupLayout, wgpu::ComputePipeline)>,
}
/// One WebGPU adapter as a fabric member: a device opened on it, the kernel
@@ -277,7 +286,12 @@ pub struct WebGpuMember {
detail: String,
max_qubits: u8,
max_x: u32,
- /// Gate records in the uniform ring: gates per submission, at most.
+ /// How circuits and lanes are planned: [`Config::PER_GATE`], or
+ /// [`Config::TILED`] for the tile implementation.
+ config: Config,
+ /// Records in each uniform ring: gate steps and tile steps per
+ /// submission, at most, and tile gates per submission unless one run
+ /// holds more.
ring_records: u32,
/// Bytes of state read back per copy.
readback_chunk: u64,
@@ -289,7 +303,7 @@ pub struct WebGpuMember {
impl std::fmt::Debug for WebGpuMember {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("WebGpuMember").field("id", &self.id).field("key", &self.key).field("implementation", &self.implementation).field("max_x", &self.max_x).field("ring_records", &self.ring_records).field("readback_chunk", &self.readback_chunk).field("detail", &self.detail).finish()
+ f.debug_struct("WebGpuMember").field("id", &self.id).field("key", &self.key).field("implementation", &self.implementation).field("config", &self.config).field("max_x", &self.max_x).field("ring_records", &self.ring_records).field("readback_chunk", &self.readback_chunk).field("detail", &self.detail).finish()
}
}
@@ -336,7 +350,7 @@ async fn open(adapter: wgpu::Adapter) -> Result {
}
});
// 5. The kernel.
- let pipelines = match kernel_pipelines(&device, Cow::Borrowed(wgsl::SHADER)).await {
+ let pipelines = match kernel_pipelines(&device, Cow::Borrowed(wgsl::SHADER), false).await {
Ok(p) => p,
Err(e) => return Err(refuse(&identity, e)),
};
@@ -361,6 +375,7 @@ async fn open(adapter: wgpu::Adapter) -> Result {
detail,
max_qubits,
max_x: wgsl::X_GROUPS_MAX,
+ config: Config::PER_GATE,
ring_records: RING_RECORDS,
readback_chunk: READBACK_CHUNK,
implementation: wgsl::implementation_id(),
@@ -446,16 +461,20 @@ impl Scopes {
}
}
-/// The kernel's two entry points with explicit layouts: `apply` over
-/// `{re, im, report: storage; gate record: uniform at a dynamic offset}`, and
-/// `probe` over `{cases: storage; probe grid: uniform}`.
-async fn kernel_pipelines(device: &wgpu::Device, source: Cow<'_, str>) -> Result {
- let storage = |binding: u32| wgpu::BindGroupLayoutEntry {
+/// The kernel's entry points with explicit layouts: `apply` over
+/// `{re, im, report: storage; gate record: uniform at a dynamic offset}`,
+/// `probe` over `{cases: storage; probe grid: uniform}`, and with `tiles`
+/// `apply_tile` over `{re, im, report: storage; tile gates: read-only storage;
+/// run record: uniform at a dynamic offset}`, four storage buffers, the
+/// downlevel limit exactly.
+async fn kernel_pipelines(device: &wgpu::Device, source: Cow<'_, str>, tiles: bool) -> Result {
+ let buffer = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
- ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: false }, has_dynamic_offset: false, min_binding_size: None },
+ ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None },
count: None,
};
+ let storage = |binding: u32| buffer(binding, false);
let uniform = |binding: u32, dynamic: bool, size: u64| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
@@ -482,8 +501,16 @@ async fn kernel_pipelines(device: &wgpu::Device, source: Cow<'_, str>) -> Result
};
let apply = pipeline(&apply_layout, "apply");
let probe = pipeline(&probe_layout, "probe");
+ let tile = tiles.then(|| {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("wai-quantum apply_tile"),
+ entries: &[storage(0), storage(1), storage(2), buffer(6, true), uniform(7, true, wgsl::TILE_RUN_RECORD_BYTES as u64)],
+ });
+ let tile = pipeline(&layout, "apply_tile");
+ (layout, tile)
+ });
scopes.close().await?;
- Ok(Pipelines { apply_layout, probe_layout, apply, probe })
+ Ok(Pipelines { apply_layout, probe_layout, apply, probe, tile })
}
/// Resolves when `map_async`'s callback has run. Natively it waits on its own
@@ -676,9 +703,16 @@ impl WebGpuMember {
self.settled()
}
+ /// How this member plans circuits and lanes: [`Config::PER_GATE`], or
+ /// [`Config::TILED`] for the tile implementation ([`Self::tiled`]).
+ pub fn config(&self) -> Config {
+ self.config
+ }
+
/// The kernel on any state of `2^n` lanes (`n ≥ 1`) and any words, each
- /// gate's index its position: what [`lanes::run_words`] gives, with words
- /// it calls malformed refused as [`WebGpuError::Internal`].
+ /// gate's index its position, planned with [`Self::config`]: what
+ /// [`lanes::run_words`] gives, with words it calls malformed refused as
+ /// [`WebGpuError::Internal`].
pub async fn run_lanes(&self, state: &[Lane], gates: &[GateWords]) -> Result {
let len = state.len();
if len < 2 || !len.is_power_of_two() {
@@ -688,12 +722,16 @@ impl WebGpuMember {
if n > u32::from(self.max_qubits) {
return Err(WebGpuError::TooWide { n_qubits: n.min(255) as u8, max: self.max_qubits });
}
- let gates: Vec<(GateWords, u32)> = gates.iter().enumerate().map(|(k, w)| (*w, u32::try_from(k).unwrap_or(u32::MAX))).collect();
- self.run(n as u8, Init::Lanes(state), &gates).await
+ let planned: Vec = gates.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, u32::try_from(k).unwrap_or(u32::MAX))).collect();
+ let p = plan::schedule(n as u8, 0, planned, &self.config);
+ self.run(n as u8, Init::Lanes(state), &p.gates, &p.steps).await
}
- /// A compiled program ([`plan::compile`]): the lanes and the first op
- /// whose exact result left 32 bits, as [`plan::execute`] gives them.
+ /// A planned program ([`plan::compile`], [`plan::compile_with`],
+ /// [`plan::schedule`]): the lanes and the first op whose exact result left
+ /// 32 bits, as [`plan::execute`] gives them. Tile steps need the tile
+ /// implementation ([`Self::tiled`]); a member without it, or a step it
+ /// cannot lay out, is refused as [`WebGpuError::Internal`].
pub async fn run_program(&self, p: &Program) -> Result {
let n = p.n_qubits;
if n == 0 {
@@ -705,24 +743,15 @@ impl WebGpuMember {
if n > MAX_QUBITS || p.start >= 1usize << n {
return Err(WebGpuError::Internal(2));
}
- let mut gates = Vec::new();
- gates.try_reserve_exact(p.steps.len()).map_err(|e| WebGpuError::Memory(e.to_string()))?;
- for step in &p.steps {
- match *step {
- Step::Gate { gate } => {
- let g = p.gates.get(gate as usize).ok_or(WebGpuError::Internal(4))?;
- gates.push((g.words, g.index));
- }
- }
- }
- self.run(n, Init::Basis(p.start), &gates).await
+ self.run(n, Init::Basis(p.start), &p.gates, &p.steps).await
}
/// Equal to `c.simulate_from(start)`, or a typed refusal: what the CPU
/// reference refuses ([`WebGpuError::Circuit`]), more qubits than the
- /// member holds, or a result that leaves 32 bits.
+ /// member holds, or a result that leaves 32 bits. Planned with
+ /// [`Self::config`].
pub async fn simulate_from(&self, c: &Circuit, start: usize) -> Result {
- let p = plan::compile(c, start).map_err(WebGpuError::Circuit)?;
+ let p = plan::compile_with(c, start, &self.config).map_err(WebGpuError::Circuit)?;
if p.n_qubits > self.max_qubits {
return Err(WebGpuError::TooWide { n_qubits: p.n_qubits, max: self.max_qubits });
}
@@ -742,14 +771,15 @@ impl WebGpuMember {
}
/// The certificate of [`apply_kernel`]`(gs)` as [`Self::implementation`]
- /// on this member, over `corpus`: each circuit run from `|0…0⟩` against the
- /// native simulator. A range report inside a corpus is an error: its
- /// circuits are valid and within the horizon.
+ /// on this member, over `corpus`: each circuit planned with
+ /// [`Self::config`] and run from `|0…0⟩`, against the native simulator. A
+ /// range report inside a corpus is an error: its circuits are valid and
+ /// within the horizon.
pub async fn certify(&self, gs: Gateset, corpus: Corpus) -> Result {
let mut cert = Certification::new();
for c in corpus.circuits(gs) {
let reference = lanes::reference_output(&c).map_err(WebGpuError::Circuit)?;
- let run = self.run_program(&plan::compile(&c, 0).map_err(WebGpuError::Circuit)?).await?;
+ let run = self.run_program(&plan::compile_with(&c, 0, &self.config).map_err(WebGpuError::Circuit)?).await?;
if let Some(gate) = run.out_of_range {
return Err(WebGpuError::OutOfRange { gate });
}
@@ -794,14 +824,34 @@ impl WebGpuMember {
/// The same device running another kernel text, as implementation
/// `"mutant"`: for showing that a device running an edited kernel is
- /// caught. Fails if the text does not compile.
+ /// caught. On a tile member ([`Self::tiled`]) the text must hold
+ /// `apply_tile` too. Fails if the text does not compile.
#[doc(hidden)]
pub async fn with_shader(&self, source: &str) -> Result {
self.alive()?;
- let pipelines = kernel_pipelines(&self.device, Cow::Owned(source.to_string())).await?;
+ let pipelines = kernel_pipelines(&self.device, Cow::Owned(source.to_string()), self.pipelines.tile.is_some()).await?;
Ok(WebGpuMember { pipelines, implementation: "mutant".into(), ..self.clone() })
}
+ /// The same device running the tile implementation:
+ /// [`wgsl::TILE_SHADER`] compiled, its arithmetic probed in this process
+ /// as discovery probes the per-gate kernel's, circuits and lanes planned
+ /// with [`Config::TILED`], and implementation
+ /// [`wgsl::tile_implementation_id`]. A tile member earns its own
+ /// certificates ([`Self::certify`], [`Self::member`]); this member is
+ /// unchanged and stays the default.
+ pub async fn tiled(&self) -> Result {
+ self.alive()?;
+ let pipelines = kernel_pipelines(&self.device, Cow::Borrowed(wgsl::TILE_SHADER), true).await?;
+ let member = WebGpuMember { pipelines, implementation: wgsl::tile_implementation_id(), config: Config::TILED, ..self.clone() };
+ let pairs = probe_pairs(4096, 0x5eed);
+ let out = member.probe(&pairs).await?;
+ match first_mismatch(&pairs, &out) {
+ None => Ok(member),
+ Some((a, b)) => Err(WebGpuError::Probe { a, b }),
+ }
+ }
+
/// An error if the device has been lost.
fn alive(&self) -> Result<(), WebGpuError> {
match &lock(&self.sink).lost {
@@ -823,6 +873,13 @@ impl WebGpuMember {
Err(WebGpuError::Device(std::mem::take(&mut s.messages).join("; ")))
}
+ /// The dispatch of a tile step over `tiles` tiles: at most
+ /// [`wgsl::TILE_X_GROUPS_MAX`] workgroups along x, fewer under a forced
+ /// geometry ([`Self::with_max_x`]).
+ fn tile_grid(&self, tiles: u32) -> wgsl::TileGrid {
+ wgsl::tile_grid(tiles, self.max_x.min(wgsl::TILE_X_GROUPS_MAX))
+ }
+
fn buffer(&self, label: &str, size: u64, usage: wgpu::BufferUsages) -> wgpu::Buffer {
self.device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, usage, mapped_at_creation: false })
}
@@ -871,15 +928,45 @@ impl WebGpuMember {
Ok(())
}
- /// Runs `gates` (words and op-log index) over `2^n` lanes from `init`:
- /// one dispatch per gate, in place, in order.
- async fn run(&self, n: u8, init: Init<'_>, gates: &[(GateWords, u32)]) -> Result {
+ /// Runs `steps` over `gates` on `2^n` lanes from `init`, in place, in
+ /// order: one dispatch per step, a gate step over the whole state and a
+ /// tile step one workgroup per tile. A step the host cannot lay out (a
+ /// gate or range it does not hold, a tile size of zero or above the
+ /// state's length, a tile step on a member without the tile kernel, a
+ /// grid past the per-dimension limit) is refused as
+ /// [`WebGpuError::Internal`] before anything runs; the kernels refuse the
+ /// rest with report bit 4.
+ async fn run(&self, n: u8, init: Init<'_>, gates: &[PlannedGate], steps: &[Step]) -> Result {
self.alive()?;
let len = 1usize << n;
let plane = 4u64 << n;
let grid = wgsl::grid(1u32 << (n - 1), self.max_x);
let ring_size = u64::from(self.ring_records) * self.stride;
+ // What the host lays out: every step in range, and the tile kernel
+ // for tile steps.
+ let mut longest = 0usize;
+ for step in steps {
+ match *step {
+ Step::Gate { gate } => {
+ if gate as usize >= gates.len() {
+ return Err(WebGpuError::Internal(4));
+ }
+ }
+ Step::Tile { first, count, size } => {
+ let end = (first as usize).checked_add(count as usize);
+ let tiles = if size == 0 || size as usize > len { 0 } else { (len / size as usize) as u32 };
+ if self.pipelines.tile.is_none() || end.is_none_or(|e| e > gates.len()) || tiles == 0 || self.tile_grid(tiles).y > 65535 {
+ return Err(WebGpuError::Internal(4));
+ }
+ longest = longest.max(count as usize);
+ }
+ }
+ }
+ let tile_steps = steps.iter().any(|s| matches!(s, Step::Tile { .. }));
+ // Tile gates per submission: a ring's worth, or the longest run.
+ let tile_capacity = (self.ring_records as usize).max(longest);
+
// Buffers and the starting state.
let scopes = Scopes::push(&self.device);
let state_usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST;
@@ -917,31 +1004,127 @@ impl WebGpuMember {
},
],
});
+ // The tile kernel's own: run records in a second uniform ring, the
+ // runs' gates in a storage buffer, rewritten per submission.
+ let tiles = match (&self.pipelines.tile, tile_steps) {
+ (Some((layout, pipeline)), true) => {
+ let runs = self.buffer("wai-quantum tile runs", ring_size, wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST);
+ let tile_gates = self.buffer("wai-quantum tile gates", (tile_capacity * wgsl::TILE_GATE_RECORD_BYTES) as u64, wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST);
+ let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("wai-quantum apply_tile"),
+ layout,
+ entries: &[
+ wgpu::BindGroupEntry { binding: 0, resource: re.as_entire_binding() },
+ wgpu::BindGroupEntry { binding: 1, resource: im.as_entire_binding() },
+ wgpu::BindGroupEntry { binding: 2, resource: report.as_entire_binding() },
+ wgpu::BindGroupEntry { binding: 6, resource: tile_gates.as_entire_binding() },
+ wgpu::BindGroupEntry {
+ binding: 7,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &runs, offset: 0, size: NonZeroU64::new(wgsl::TILE_RUN_RECORD_BYTES as u64) }),
+ },
+ ],
+ });
+ Some((pipeline, runs, tile_gates, bind))
+ }
+ _ => None,
+ };
scopes.close().await?;
upload?;
- // The gates, a chunk per submission, each waited on natively before
- // the next, so that POLL_TIMEOUT bounds one chunk's GPU time.
- let per_submit = (SUBMIT_BUDGET >> (n - 1)).clamp(1, u64::from(self.ring_records)) as usize;
+ // The steps, a chunk per submission, each waited on natively before
+ // the next, so that POLL_TIMEOUT bounds one chunk's GPU time. A chunk
+ // holds at least one step, and more while its pair-updates stay within
+ // SUBMIT_BUDGET, each ring holds its records and the tile gate buffer
+ // holds the runs' gates.
+ let pairs = 1u64 << (n - 1);
+ let ring_records = self.ring_records as usize;
let stride = self.stride as usize;
- for chunk in gates.chunks(per_submit) {
- let mut records = vec![0u8; chunk.len() * stride];
- for (k, (words, index)) in chunk.iter().enumerate() {
- records[k * stride..k * stride + wgsl::GATE_RECORD_BYTES].copy_from_slice(&wgsl::gate_record(words, grid.items, grid.row_stride, *index));
+ let mut next = 0;
+ while next < steps.len() {
+ let first = next;
+ let (mut cost, mut gate_steps, mut tile_runs, mut tile_gates) = (0u64, 0usize, 0usize, 0usize);
+ while next < steps.len() {
+ let (work, is_tile, held) = match steps[next] {
+ Step::Gate { .. } => (pairs, false, 0),
+ Step::Tile { count, .. } => (pairs.saturating_mul(u64::from(count)), true, count as usize),
+ };
+ let full = cost.saturating_add(work) > SUBMIT_BUDGET || if is_tile { tile_runs == ring_records || tile_gates + held > tile_capacity } else { gate_steps == ring_records };
+ if next > first && full {
+ break;
+ }
+ cost = cost.saturating_add(work);
+ if is_tile {
+ (tile_runs, tile_gates) = (tile_runs + 1, tile_gates + held);
+ } else {
+ gate_steps += 1;
+ }
+ next += 1;
+ }
+ let chunk = &steps[first..next];
+ let mut gate_records = vec![0u8; gate_steps * stride];
+ let mut run_records = vec![0u8; tile_runs * stride];
+ let mut tile_records = Vec::with_capacity(tile_gates * wgsl::TILE_GATE_RECORD_BYTES);
+ let (mut g, mut t) = (0usize, 0usize);
+ for step in chunk {
+ match *step {
+ Step::Gate { gate } => {
+ let pg = &gates[gate as usize];
+ gate_records[g * stride..g * stride + wgsl::GATE_RECORD_BYTES].copy_from_slice(&wgsl::gate_record(&pg.words, grid.items, grid.row_stride, pg.index));
+ g += 1;
+ }
+ Step::Tile { first, count, size } => {
+ let at = (tile_records.len() / wgsl::TILE_GATE_RECORD_BYTES) as u32;
+ for pg in &gates[first as usize..(first + count) as usize] {
+ tile_records.extend_from_slice(&wgsl::tile_gate_record(pg));
+ }
+ let tg = self.tile_grid((len / size as usize) as u32);
+ run_records[t * stride..t * stride + wgsl::TILE_RUN_RECORD_BYTES].copy_from_slice(&wgsl::tile_run_record(at, count, size, tg.x, tg.tiles));
+ t += 1;
+ }
+ }
}
let scopes = Scopes::push(&self.device);
- // Ordered on the queue after the submissions that read the ring before.
- self.queue.write_buffer(&ring, 0, &records);
+ // Ordered on the queue after the submissions that read them before.
+ if !gate_records.is_empty() {
+ self.queue.write_buffer(&ring, 0, &gate_records);
+ }
+ if let Some((_, runs, tile_gate_buffer, _)) = &tiles
+ && !run_records.is_empty()
+ {
+ self.queue.write_buffer(runs, 0, &run_records);
+ self.queue.write_buffer(tile_gate_buffer, 0, &tile_records);
+ }
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("wai-quantum gates") });
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("wai-quantum gates"), timestamp_writes: None });
- pass.set_pipeline(&self.pipelines.apply);
- for k in 0..chunk.len() {
- pass.set_bind_group(0, &bind, &[(k * stride) as u32]);
- pass.dispatch_workgroups(grid.x, grid.y, 1);
+ let (mut g, mut t, mut on_tiles) = (0usize, 0usize, None);
+ for step in chunk {
+ match (*step, &tiles) {
+ (Step::Gate { .. }, _) => {
+ if on_tiles != Some(false) {
+ pass.set_pipeline(&self.pipelines.apply);
+ on_tiles = Some(false);
+ }
+ pass.set_bind_group(0, &bind, &[(g * stride) as u32]);
+ pass.dispatch_workgroups(grid.x, grid.y, 1);
+ g += 1;
+ }
+ (Step::Tile { size, .. }, Some((pipeline, _, _, tile_bind))) => {
+ if on_tiles != Some(true) {
+ pass.set_pipeline(pipeline);
+ on_tiles = Some(true);
+ }
+ let tg = self.tile_grid((len / size as usize) as u32);
+ pass.set_bind_group(0, tile_bind, &[(t * stride) as u32]);
+ pass.dispatch_workgroups(tg.x, tg.y, 1);
+ t += 1;
+ }
+ (Step::Tile { .. }, None) => unreachable!("tile steps were refused above without the tile kernel"),
+ }
}
}
let _index = self.queue.submit([encoder.finish()]);
+ drop((gate_records, run_records, tile_records));
scopes.close().await?;
#[cfg(not(target_arch = "wasm32"))]
wait(&self.device, &self.sink, _index)?;
diff --git a/wai/wai-quantum/tests/webgpu_member.rs b/wai/wai-quantum/tests/webgpu_member.rs
index 4735d32af..84ba6b9c3 100644
--- a/wai/wai-quantum/tests/webgpu_member.rs
+++ b/wai/wai-quantum/tests/webgpu_member.rs
@@ -20,11 +20,12 @@ use std::future::Future;
use std::io::Write;
use std::path::Path;
use std::sync::OnceLock;
+use std::time::Instant;
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::plan::{self, Config, PlannedGate, Program, Step};
+use wai_quantum::quantum_fabric::wgsl::{self, Detector, MUTANTS, Mutant, TILE_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};
@@ -451,36 +452,81 @@ fn g7_refusals_are_typed() {
}
}
+/// Whether a member running `mutant`'s text is caught by the mutant's
+/// detector.
+fn caught(mm: &WebGpuMember, mutant: &Mutant, key: &str) -> bool {
+ match mutant.detector {
+ Detector::Probe => {
+ let edges = probe_pairs(0, 0);
+ 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 {key}: {differing} of 484 edge pairs differ", mutant.name);
+ differing >= 1
+ }
+ Detector::Certificate => Gateset::ALL.into_iter().all(|gs| match attempt(mm.certify(gs, RANDOM_V1)) {
+ Err(e) => {
+ println!("webgpu mutant {} on {key}: {gs:?} certification refused: {e}", mutant.name);
+ 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)),
+ Detector::Race => {
+ // A race need not show on every run: three tries per size.
+ let mut differing = Vec::new();
+ for n in [10u8, 11] {
+ let (state, words) = wgsl::race_witness(n);
+ let want = lanes::run_words(&state, &words).expect("valid shapes");
+ for _ in 0..3 {
+ match attempt(mm.run_lanes(&state, &words)) {
+ Ok(got) => differing.push(got.amps.iter().zip(&want.amps).filter(|(a, b)| a != b).count() + usize::from(got.out_of_range != want.out_of_range)),
+ Err(e) => {
+ println!("webgpu mutant {} on {key}: n = {n} refused: {e}", mutant.name);
+ differing.push(usize::MAX);
+ }
+ }
+ }
+ }
+ println!("webgpu mutant {} on {key}: lanes differing per run of the race witness: {differing:?}", mutant.name);
+ differing.iter().any(|&d| d > 0)
+ }
+ Detector::Malformed => attempt(mm.run_program(&wgsl::malformed_tile_run())) != Err(WebGpuError::Internal(4)),
+ }
+}
+
#[test]
fn g8_mutants_are_killed() {
- let edges = probe_pairs(0, 0);
- assert_eq!(edges.len(), 484);
+ assert_eq!(probe_pairs(0, 0).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);
+ assert!(caught(&mm, mutant, m.key()), "{}: 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()));
+
+ // The tile kernel's mutants, on the tile member. First what the
+ // detectors rely on: the kernel as written reports the range, refuses
+ // the malformed run and runs the race witness as the CPU does.
+ let tiled = run(m.tiled());
+ assert_eq!(run(tiled.run_lanes(&Z_STATE, &[z_words()])).out_of_range, Some(0), "{}", m.key());
+ assert_eq!(attempt(tiled.run_program(&wgsl::malformed_tile_run())).unwrap_err(), WebGpuError::Internal(4), "{}", m.key());
+ for n in [10u8, 11] {
+ let (state, words) = wgsl::race_witness(n);
+ assert_eq!(run(tiled.run_lanes(&state, &words)), lanes::run_words(&state, &words).unwrap(), "{} race witness n = {n}", m.key());
+ }
+ let mut killed = 0;
+ for mutant in &TILE_MUTANTS {
+ let mm = attempt(tiled.with_shader(&wgsl::mutate_tile(mutant))).unwrap_or_else(|e| panic!("{}: tile mutant {} does not compile: {e}", m.key(), mutant.name));
+ assert_eq!((mm.implementation(), mm.config()), ("mutant", Config::TILED));
+ assert!(caught(&mm, mutant, m.key()), "{}: tile mutant {} survived", m.key(), mutant.name);
+ killed += 1;
+ }
+ assert_eq!(killed, TILE_MUTANTS.len());
+ report(&format!("webgpu tile mutants: key={} killed {killed} of {}", m.key(), TILE_MUTANTS.len()));
}
}
@@ -541,3 +587,147 @@ fn g11_native_futures_resolve_in_one_poll() {
// Every other test reached its results through `blocking::resolve`.
report("webgpu futures: one poll each");
}
+
+/// The milliseconds `f` takes, at best over `tries`, and its last result.
+fn timed(tries: usize, mut f: impl FnMut() -> T) -> (f64, T) {
+ let mut best = f64::INFINITY;
+ let mut last = None;
+ for _ in 0..tries {
+ let t = Instant::now();
+ let v = f();
+ best = best.min(t.elapsed().as_secs_f64() * 1e3);
+ last = Some(v);
+ }
+ (best, last.expect("at least one try"))
+}
+
+#[test]
+fn g12_tiles_match_per_gate() {
+ for m in members() {
+ let tiled = run(m.tiled());
+ assert_eq!((tiled.id(), tiled.implementation(), tiled.config()), (m.id(), wgsl::tile_implementation_id().as_str(), Config::TILED));
+ assert!(tiled.implementation().starts_with("wgsl-lanes32-tile/"));
+
+ // Any state and any words, mapped through tile steps: the tile member
+ // equals the per-gate member and the CPU, lanes and report.
+ let mut s = 0x7469_6c65_6432_u64;
+ let (mut flagged, mut clean, mut runs) = (0, 0, 0);
+ for case in 0..2000 {
+ let n = 1 + (next(&mut s) % 12) as u32;
+ let full = case % 2 == 0;
+ let value = |s: &mut u64| -> i32 { if full { next(s) as i32 } else { within(s, 1 << 22) } };
+ let entry = |s: &mut u64| -> u32 { if full { next(s) as u32 } else { within(s, 1 << 29) as u32 } };
+ let state: Vec = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
+ let gates: Vec = (0..1 + next(&mut s) % 24)
+ .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));
+ // Half the gates diagonal, on every target.
+ if next(&mut s) % 2 == 0 {
+ w[2..6].fill(0);
+ }
+ GateWords { m: w, tbit: 1 << t, ctrl }
+ })
+ .collect();
+ let planned: Vec = gates.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, k as u32)).collect();
+ if plan::schedule(n as u8, 0, planned, &Config::TILED).steps.iter().any(|st| matches!(st, Step::Tile { count, .. } if *count > 1)) {
+ runs += 1;
+ }
+ let want = lanes::run_words(&state, &gates).expect("valid shapes");
+ let got = run(tiled.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());
+ assert!(run(m.run_lanes(&state, &gates)) == got, "{}: case {case}, n = {n}: per-gate and tiled differ", m.key());
+ if want.out_of_range.is_some() {
+ flagged += 1;
+ } else {
+ clean += 1;
+ }
+ }
+ assert!(flagged >= 100 && clean >= 100 && runs >= 1000, "{}: {flagged} flagged, {clean} clean, {runs} with a run of two gates or more", m.key());
+ report(&format!("webgpu tile kernel: key={} ok", m.key()));
+
+ // The four pinned certificates, under the tile implementation.
+ for (gs, random_pin, fabric_pin) in PINS {
+ for (corpus, cases, pin) in [(RANDOM_V1, 200, random_pin), (FABRIC_DEFAULT, 224, fabric_pin)] {
+ let c = blocking::certify(&tiled, gs, corpus).unwrap_or_else(|e| panic!("{} tiled {gs:?} {}: {e}", m.key(), corpus.name()));
+ assert_eq!((c.parity, c.cases), (Parity::BitExact, cases), "{} tiled {gs:?} {}", m.key(), corpus.name());
+ assert_eq!(hex(&c.corpus), pin, "{} tiled {gs:?} {}", m.key(), corpus.name());
+ assert_eq!((c.kernel.as_str(), c.implementation.as_str(), c.member.as_str()), (apply_kernel(gs), tiled.implementation(), m.id()));
+ assert_eq!(c.generator, corpus.generator(gs));
+ 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)
+ ));
+ }
+ }
+ // fabric-v1 (tiles of 1024, up to 128 of them) on forced geometries: one
+ // workgroup to a row, and three (a ragged last row), with rings of two
+ // records and the planes read back in pieces.
+ for max_x in [1, 3] {
+ let forced = tiled.with_max_x(max_x).with_ring_records(2).with_readback_chunk(12 * 1024 + 4);
+ for (gs, _, pin) in PINS {
+ let c = run(forced.certify(gs, FABRIC_DEFAULT));
+ assert_eq!((c.parity, hex(&c.corpus)), (Parity::BitExact, pin.to_string()), "{} tiled max_x {max_x} {gs:?}", m.key());
+ }
+ }
+ report(&format!("webgpu tile geometry: key={} ok", m.key()));
+
+ // The conformance vectors.
+ let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance");
+ let mut total = 0;
+ for (dir, manifest) in [("vectors", "expected_hashes.json"), ("v2/vectors", "v2/expected_hashes.json")] {
+ let manifest: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(root.join(manifest)).expect("the manifest reads")).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 (c, _) = from_wqc(&std::fs::read(&path).unwrap()).unwrap_or_else(|e| panic!("{dir}/{name}: {e}"));
+ let state = run(tiled.simulate(&c));
+ assert_eq!(Some(hex(&state.statevector_hash()).as_str()), manifest[&name]["statevector_hash"].as_str(), "{} tiled {dir}/{name}", m.key());
+ total += 1;
+ }
+ }
+ report(&format!("webgpu tile vectors: key={} {total} ok", m.key()));
+
+ // Repeated runs agree.
+ let c = random_circuit(18, 200, 0x7265_7065_6174, Gateset::V1);
+ let native = hex(&c.simulate().unwrap().statevector_hash());
+ let hashes: Vec = (0..5).map(|_| hex(&blocking::simulate_from(&tiled, &c, 0).unwrap().statevector_hash())).collect();
+ assert!(hashes.iter().all(|h| *h == native), "{}: {hashes:?}, native {native}", m.key());
+ report(&format!("webgpu tile repeated: key={} 5 runs, one hash", m.key()));
+
+ // Timing, per-gate and tiled on the same circuits: printed, never
+ // asserted. The hashes must agree.
+ // On 10 qubits every gate fits one tile: runs of up to 1024 gates.
+ let long = random_circuit(10, 4000, 0x6c6f_6e67, Gateset::V2);
+ for (what, c) in [
+ ("qft", Circuit::qft(16)),
+ ("random", random_circuit(16, 400, 0x7469_6d65, Gateset::V2)),
+ ("random", random_circuit(20, 400, 0x7469_6d65 + 1, Gateset::V2)),
+ ("random", random_circuit(22, 200, 0x7469_6d65 + 2, Gateset::V2)),
+ ("long", long),
+ ] {
+ let steps = plan::compile_with(&c, 0, &Config::TILED).unwrap().steps.len();
+ let (per_gate_ms, a) = timed(3, || blocking::simulate_from(m, &c, 0).unwrap().statevector_hash());
+ let (tiled_ms, b) = timed(3, || blocking::simulate_from(&tiled, &c, 0).unwrap().statevector_hash());
+ assert_eq!(hex(&a), hex(&b), "{} {what} n = {}", m.key(), c.n_qubits);
+ report(&format!(
+ "webgpu timing: key={} {what} n={} gates={}: per-gate {per_gate_ms:.1} ms ({} dispatches), tiled {tiled_ms:.1} ms ({steps} dispatches)",
+ m.key(),
+ c.n_qubits,
+ c.ops.len(),
+ c.ops.len()
+ ));
+ }
+ }
+}
diff --git a/wai/wai-web/demo/README.md b/wai/wai-web/demo/README.md
index 0f630e5fb..6fa760370 100644
--- a/wai/wai-web/demo/README.md
+++ b/wai/wai-web/demo/README.md
@@ -128,9 +128,11 @@ WebGPU. A bit-exact member reproduces the CPU reference's pinned corpus digests,
`quantum-conformance/fabric/expected.json`.
The page runs `quantum_webgpu_worker.js` in a module worker: the four pinned
-certificates (both gatesets, random-v1 and fabric-v1), every vector of the quantum
-corpus, every reject file (each refused with its listed class), and a circuit one qubit
-wider than the member holds. A second worker repeats the last check with the adapter
+certificates (both gatesets, random-v1 and fabric-v1) and every vector of the quantum
+corpus, on the member and again on the same adapter running the tile implementation
+(`tiled()`: runs of gates applied tile by tile, one dispatch per run, whose barrier the
+page's WGSL compiler holds to the uniformity rules); then every reject file (each refused
+with its listed class), and a circuit one qubit wider than the member holds. A second worker repeats the last check with the adapter
offering only the WebGPU default buffer limits, where the member holds 25 qubits and
refuses 26 as too wide.
diff --git a/wai/wai-web/demo/quantum_webgpu.html b/wai/wai-web/demo/quantum_webgpu.html
index 0d08ec5d3..80d802a7f 100644
--- a/wai/wai-web/demo/quantum_webgpu.html
+++ b/wai/wai-web/demo/quantum_webgpu.html
@@ -28,6 +28,8 @@
against the CPU reference on two corpora per gateset. A bit-exact member reproduces the reference's
pinned corpus digests (quantum-conformance/fabric/expected.json); every vector of the
quantum corpus must reproduce its statevector hash, and every reject file must be refused with its class.
+ The same adapter then runs the tile implementation (runs of gates applied tile by tile, one dispatch
+ per run), certified against the same digests.
starting
@@ -57,7 +59,11 @@ function show(m) {
`certificate ${m.label}: ${m.got.parity}, ${m.got.cases} cases, corpus ${m.got.corpus_hex}${m.got.corpus_hex === m.pin ? " (the pin)" : `, pinned ${m.pin}`}`);
break;
case "vector":
- line(m.got === m.want ? "ok" : "bad", `${m.label}: ${m.error ? `refused: ${m.error}` : `statevector ${m.got}${m.got === m.want ? "" : `, manifest ${m.want}`}`}`);
+ line(m.got === m.want ? "ok" : "bad", `${m.tile ? "tile " : ""}${m.label}: ${m.error ? `refused: ${m.error}` : `statevector ${m.got}${m.got === m.want ? "" : `, manifest ${m.want}`}`}`);
+ break;
+ case "tiled":
+ if (m.error) line("bad", `tile member: ${m.error}`);
+ else line("info", `tile member ${m.id}: implementation ${m.implementation}`);
break;
case "reject": {
const ok = !!m.error && m.error.startsWith(`WQC parse (${m.want}): `);
diff --git a/wai/wai-web/demo/quantum_webgpu_worker.js b/wai/wai-web/demo/quantum_webgpu_worker.js
index 6f41a7c66..899f05609 100644
--- a/wai/wai-web/demo/quantum_webgpu_worker.js
+++ b/wai/wai-web/demo/quantum_webgpu_worker.js
@@ -6,6 +6,9 @@
// four certificates quantum-conformance/fabric/expected.json pins;
// - runs every vector of quantum-conformance/vectors/ and v2/vectors/ on the
// member to its statevector hash;
+// - does both again on the same adapter running the tile implementation
+// (member.tiled()), whose kernel this worker's WGSL compiler also holds to the
+// uniformity rules (messages with tile: true);
// - offers it every file of quantum-conformance/reject/, each of which must be
// refused with its listed class;
// - offers it a circuit one qubit wider than it holds: refused as too-wide, or as
@@ -114,6 +117,31 @@ async function tooWide(member, limits) {
}
}
+// The pinned certificates, then every vector of both trees, on `member`.
+async function certifyAndRun(member, expected, tile) {
+ for (const c of expected.certificates) {
+ const label = `${tile ? "tile " : ""}${c.kernel} ${c.corpus}`;
+ try {
+ const got = JSON.parse(await member.certify(c.gateset, c.corpus, BigInt(c.seed), c.cases, c.wide));
+ post({ type: "certificate", tile, label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, got });
+ } catch (e) {
+ post({ type: "certificate", tile, label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, error: message(e) });
+ }
+ }
+ for (const prefix of ["", "v2/"]) {
+ const manifest = await fetchJson(new URL(`${prefix}expected_hashes.json`, CORPUS));
+ for (const [file, entry] of Object.entries(manifest)) {
+ const label = prefix + file;
+ try {
+ const bytes = await fetchBytes(new URL(`${prefix}vectors/${file}`, CORPUS));
+ post({ type: "vector", tile, label, want: entry.statevector_hash, got: await member.statevector_hash_hex(bytes) });
+ } catch (e) {
+ post({ type: "vector", tile, label, want: entry.statevector_hash, error: message(e) });
+ }
+ }
+ }
+}
+
async function run() {
if (!navigator.gpu) {
post({ type: "no-adapter", why: "navigator.gpu is absent in this worker" });
@@ -146,31 +174,18 @@ async function run() {
return;
}
- // The four certificates.
+ // The four certificates and every vector of both trees, on the per-gate
+ // member and then on the tile member.
const expected = await fetchJson(new URL("fabric/expected.json", CORPUS));
- for (const c of expected.certificates) {
- const label = `${c.kernel} ${c.corpus}`;
- try {
- const got = JSON.parse(await member.certify(c.gateset, c.corpus, BigInt(c.seed), c.cases, c.wide));
- post({ type: "certificate", label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, got });
- } catch (e) {
- post({ type: "certificate", label, gateset: c.gateset, corpus: c.corpus, pin: c.corpus_hex, error: message(e) });
- }
- }
-
- // Every vector of both trees.
- for (const prefix of ["", "v2/"]) {
- const manifest = await fetchJson(new URL(`${prefix}expected_hashes.json`, CORPUS));
- for (const [file, entry] of Object.entries(manifest)) {
- const label = prefix + file;
- try {
- const bytes = await fetchBytes(new URL(`${prefix}vectors/${file}`, CORPUS));
- post({ type: "vector", label, want: entry.statevector_hash, got: await member.statevector_hash_hex(bytes) });
- } catch (e) {
- post({ type: "vector", label, want: entry.statevector_hash, error: message(e) });
- }
- }
+ await certifyAndRun(member, expected, false);
+ let tiled;
+ try {
+ tiled = await member.tiled();
+ post({ type: "tiled", id: tiled.member_id(), implementation: tiled.implementation() });
+ } catch (e) {
+ post({ type: "tiled", error: message(e) });
}
+ if (tiled) await certifyAndRun(tiled, expected, true);
// Every reject file.
const listing = await fetchJson(new URL("reject/reject.json", CORPUS));
diff --git a/wai/wai-web/src/quantum_webgpu.rs b/wai/wai-web/src/quantum_webgpu.rs
index 60fc2571d..7b9c826b3 100644
--- a/wai/wai-web/src/quantum_webgpu.rs
+++ b/wai/wai-web/src/quantum_webgpu.rs
@@ -9,7 +9,10 @@
//!
//! The kernel is the same WGSL text the native member runs, compiled here by
//! the browser's own WGSL compiler, so a bit-exact certificate from a page is
-//! evidence from an independent translator. The corpus digest covers inputs
+//! evidence from an independent translator. [`QuantumWebGpu::tiled`] gives the
+//! same adapter running the tile implementation, whose barrier the browser's
+//! compiler holds to WGSL's uniformity rules, which the native translator does
+//! not check. The corpus digest covers inputs
//! and outputs only, so a bit-exact member reproduces the CPU reference's
//! pinned digests. Certificates are earned in the page that runs the work.
//!
@@ -127,11 +130,25 @@ impl QuantumWebGpu {
self.member.max_qubits()
}
- /// The kernel implementation the member runs (`wgsl-lanes32/<16 hex>`).
+ /// The kernel implementation the member runs (`wgsl-lanes32/<16 hex>`, or
+ /// `wgsl-lanes32-tile/<16 hex>` for a tile member).
pub fn implementation(&self) -> String {
self.member.implementation().to_string()
}
+ /// The same adapter running the tile implementation: the tile kernel
+ /// compiled by the page's WebGPU, its arithmetic probed in this page, and
+ /// circuits planned in tile runs. Resolves to a `QuantumWebGpu` that
+ /// certifies and runs as this one does; rejects with
+ /// `tiled (): `.
+ pub fn tiled(&self) -> js_sys::Promise {
+ let member = Rc::clone(&self.member);
+ future_to_promise(async move {
+ let tiled = member.tiled().await.map_err(|e| member_refusal("tiled", &e))?;
+ Ok(QuantumWebGpu { member: Rc::new(tiled) }.into())
+ })
+ }
+
/// Certify the gate kernel of `gateset` (1 for `wai.quantum.circuit`, 2 for
/// `wai.quantum.circuit2`) on this member over a corpus: `"random-v1"`
/// (`seed`, `cases`; `wide` must be 0) or `"fabric-v1"` (`seed`, `cases`,
--
2.54.0 (Apple Git-157)
From 897ab7d80802ac55c47d6ed3b90861ff6cc43e8a Mon Sep 17 00:00:00 2001
From: Transaction Science
Date: Sun, 11 Oct 2026 23:12:21 -0400
Subject: [PATCH 3/4] wai-quantum WebGPU member: tile review fixes
From an independent review of the tile implementation (no input gave wrong
bits); each finding fixed:
- expected.json is authoritative: tests/fabric_expected.rs (excluded from the
package, so publishing is unaffected; cargo drops its target and verification
builds the library only) requires quantum-conformance/fabric/expected.json to
be exactly the four certificates of the pinned corpora (gateset, corpus, seed,
cases, wide circuits, the digest the CPU computes) and both implementation ids;
a missing file fails. The browser job runs it in place of the grep step, whose
comment now says what is checked. A copy giving fabric-v1 random-v1's digests
fails it.
- The checker's step 6 adds a uniformity taint check: a barrier must not sit
under a condition, or after a return, break or continue under a condition,
that depends on the invocation's ids or on memory other invocations write
(read-write storage, workgroup memory, atomics), directly or through a
variable or a call. Uniform buffers, read-only storage, buffer lengths and
the workgroup id, count and size are uniform. Planted early returns on
local_invocation_index (the reviewer's variant among them), on a read-write
load, through a variable and through a call each fail; the real shaders pass.
- The tile kernel refuses a run record (bit 4, nothing touched) unless its
tiles cover each state plane exactly and its grid (num_workgroups, one deep)
holds every tile once with x_groups to a row. The tile implementation id is
now wgsl-lanes32-tile/54e443520c020013, re-pinned in wgsl.rs and
expected.json.
- PlannedGate::is_well_formed: diag must match the words and the op index must
not be u32::MAX (the tile kernel's "no op"). plan::execute_from returns None
and the member refuses Internal(4) otherwise; compile never makes either.
- The member splits a tile step into dispatches of at most SUBMIT_BUDGET
pair-updates (splitting a run is exact), so every submission keeps the budget
the module and POLL_TIMEOUT documents state.
- g12 adds, for the tile member: starts other than zero; a first out-of-range
op in a later submission with circuit/1 P(19) as filler (never skipped); a
600-gate run at 20 qubits that the member splits; ill-formed planned gates
refused by both implementations; and, inside WAI_QUANTUM_WEBGPU_WIDE only,
the widest sizes (run cap 16, grid (8, 8192) at 26 qubits), which the member
script then requires.
- Placement's tie order is documented as it is: with both implementations'
entries for one member and no energy figures, place picks the tile
implementation (its name sorts first); discovery opens per-gate members,
which are the fallback. A test pins the order.
- wai/wai-web/src/lib.rs is in the workflow's paths; the wasip2 job requires
the new unit test by name.
Co-Authored-By: Claude Opus 5.5
---
.github/workflows/wai-conformance.yml | 1 +
.github/workflows/wai-quantum-webgpu.yml | 25 +-
scripts/wai-quantum-webgpu-member.sh | 2 +
wai/CHANGELOG.md | 47 ++--
wai/quantum-conformance/fabric/expected.json | 2 +-
wai/tools/quantum-wgsl-check/src/main.rs | 258 +++++++++++++++++-
wai/wai-quantum/Cargo.toml | 7 +
.../src/quantum_fabric/lanes32_tile.wgsl | 22 +-
wai/wai-quantum/src/quantum_fabric/plan.rs | 46 +++-
wai/wai-quantum/src/quantum_fabric/wgsl.rs | 47 +++-
wai/wai-quantum/src/quantum_fabric_webgpu.rs | 61 ++++-
wai/wai-quantum/tests/fabric_expected.rs | 61 +++++
wai/wai-quantum/tests/webgpu_member.rs | 70 +++++
13 files changed, 570 insertions(+), 79 deletions(-)
create mode 100644 wai/wai-quantum/tests/fabric_expected.rs
diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml
index 717013f7b..ceaed87da 100644
--- a/.github/workflows/wai-conformance.yml
+++ b/.github/workflows/wai-conformance.yml
@@ -597,6 +597,7 @@ jobs:
quantum_fabric::wgsl::tests::the_cpu_reference_is_placed_first_when_nothing_is_metered \
quantum_fabric::wgsl::tests::mutants_apply_once \
quantum_fabric::wgsl::tests::mutant_witnesses_exist \
+ quantum_fabric::wgsl::tests::placement_breaks_an_implementation_tie_by_name \
quantum_fabric::wgsl::tests::tile_implementation_id_is_pinned \
quantum_fabric::wgsl::tests::tile_records_layout \
quantum_fabric::wgsl::tests::tile_grids_cover_every_tile_once \
diff --git a/.github/workflows/wai-quantum-webgpu.yml b/.github/workflows/wai-quantum-webgpu.yml
index d7848c1f6..981d3c7f8 100644
--- a/.github/workflows/wai-quantum-webgpu.yml
+++ b/.github/workflows/wai-quantum-webgpu.yml
@@ -48,6 +48,7 @@ on:
- 'wai/wai-web/Cargo.toml'
- 'wai/wai-web/Cargo.lock'
- 'wai/wai-web/src/quantum_webgpu.rs'
+ - 'wai/wai-web/src/lib.rs'
- 'wai/wai-web/demo/quantum_webgpu*'
- 'scripts/wai-quantum-webgpu-member.sh'
- '.github/workflows/wai-quantum-webgpu.yml'
@@ -66,6 +67,7 @@ on:
- 'wai/wai-web/Cargo.toml'
- 'wai/wai-web/Cargo.lock'
- 'wai/wai-web/src/quantum_webgpu.rs'
+ - 'wai/wai-web/src/lib.rs'
- 'wai/wai-web/demo/quantum_webgpu*'
- 'scripts/wai-quantum-webgpu-member.sh'
- '.github/workflows/wai-quantum-webgpu.yml'
@@ -240,24 +242,19 @@ jobs:
- name: Put cargo on PATH
shell: bash
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- # The page's pins are the ones the crate's tests hold, so that editing the page's
- # file alone cannot make a wrong member pass.
+ # The page's file is held to the crate by wai-quantum's tests/fabric_expected.rs:
+ # exactly the four certificates of the pinned corpora (gateset, corpus, seed,
+ # cases, wide circuits and the digest the CPU computes) and both implementation
+ # ids. Editing the file alone, say giving fabric-v1 random-v1's digests so that
+ # no circuit wider than one tile runs, fails here.
- name: The page's pins are the crate's
shell: bash
+ working-directory: wai/wai-quantum
run: |
set -euo pipefail
- f=wai/quantum-conformance/fabric/expected.json
- digests="$(python3 -I -c 'import json, sys; print("\n".join(c["corpus_hex"] for c in json.load(open(sys.argv[1]))["certificates"]))' "$f")"
- [ "$(printf '%s\n' "$digests" | grep -cE '^[0-9a-f]{64}$')" -eq 4 ] || { echo "::error::$f does not list four digests"; exit 1; }
- for d in $digests; do
- grep -q "\"$d\"" wai/wai-quantum/src/quantum_fabric.rs || { echo "::error::$d is not pinned in wai-quantum"; exit 1; }
- done
- implementation="$(python3 -I -c 'import json, sys; print(json.load(open(sys.argv[1]))["implementation"])' "$f")"
- grep -q "PINNED_IMPLEMENTATION: &str = \"$implementation\";" wai/wai-quantum/src/quantum_fabric/wgsl.rs \
- || { echo "::error::$implementation is not the pinned implementation id"; exit 1; }
- tile="$(python3 -I -c 'import json, sys; print(json.load(open(sys.argv[1]))["tile_implementation"])' "$f")"
- grep -q "PINNED_TILE_IMPLEMENTATION: &str = \"$tile\";" wai/wai-quantum/src/quantum_fabric/wgsl.rs \
- || { echo "::error::$tile is not the pinned tile implementation id"; exit 1; }
+ nice -n 10 cargo test --locked --features quantum_fabric --test fabric_expected 2>&1 | tee "$RUNNER_TEMP/fabric-expected.log"
+ grep -q '^test the_browser_legs_pins_are_the_crates \.\.\. ok$' "$RUNNER_TEMP/fabric-expected.log" \
+ || { echo "::error::the pins test did not run"; exit 1; }
# Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the crate
# must match exactly.
- name: wasm-bindgen CLI 0.2.127
diff --git a/scripts/wai-quantum-webgpu-member.sh b/scripts/wai-quantum-webgpu-member.sh
index 459e07dfb..c408c34c5 100755
--- a/scripts/wai-quantum-webgpu-member.sh
+++ b/scripts/wai-quantum-webgpu-member.sh
@@ -51,5 +51,7 @@ for key in $(tr ',' '\n' <<<"$WAI_QUANTUM_WEBGPU_EXPECT" | sort -u); do
if [ "${WAI_QUANTUM_WEBGPU_WIDE:-0}" = 1 ] && [ "${key%%-*}" = gpu ]; then
grep -qE "^webgpu wide: key=$key n=2[56] matches native$" "$WAI_QUANTUM_WEBGPU_REPORT" \
|| { echo "::error::$key: no wide run at the member's full size"; exit 1; }
+ grep -qE "^webgpu tile wide: key=$key n=2[56] matches native$" "$WAI_QUANTUM_WEBGPU_REPORT" \
+ || { echo "::error::$key: no wide tile run at the member's full size"; exit 1; }
fi
done
diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md
index 8f766e918..3d23ccd4f 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -79,27 +79,34 @@ Additive. The default features, `full`, and every existing pin and digest are un
if the adapters found differ, if any test did not run, if a declared adapter lacks a
bit-exact certificate for each pinned digest, or if a shader mutant survives. The
`wasm32-wasip2` job requires the fabric's new unit tests by name.
-- **Tile runs: a second implementation, `wgsl-lanes32-tile/de6fb388454a0144`.**
+- **Tile runs: a second implementation, `wgsl-lanes32-tile/54e443520c020013`.**
`plan::Config` (`PER_GATE`, `TILED`; `tile_size`, `run_cap`), `plan::Step::Tile`,
- `compile_with`, `schedule`, `execute_from`, `PlannedGate::{new, is_identity}` and
- `TILE_BITS_MAX`; `PLAN_REVISION` is 2. A tiled plan applies each maximal run of
- consecutive gates that act inside a tile (diagonal gates on any target, others with
- their target inside the tile) to tiles of up to 1024 amplitudes, in op-log order,
- and leaves out exact identities only (circuit/1's `P(19..=32)` is `2^30 − 1`, not the
- identity, and stays). The CPU executor mirrors a tile kernel's index arithmetic and
- equals the per-gate program on any words; every configuration certifies with the
- lanes32 pins. `wgsl::TILE_SHADER` (`lanes32.wgsl` followed by `lanes32_tile.wgsl`,
- entry point `apply_tile`: one workgroup of 256 per tile, the tile updated in place in
- the state planes, a storage barrier between gates, the tile size in the run record,
- never in the text) has its own id (`tile_implementation_id`), records
- (`tile_gate_record`, `tile_run_record`), grid (`tile_grid`) and mutants
- (`TILE_MUTANTS`, with the detectors `Race` and `Malformed`, `race_witness`,
- `malformed_tile_run`). `WebGpuMember::tiled()` gives the same device running it,
- probed and certified on its own; the per-gate member stays the default and the
- fallback. The checker holds `TILE_SHADER` to its own rules, barrier placement
- included, and its SPIR-V to the validator; the member's device tests, the member
- script and the browser leg require the four pinned digests under both
- implementations and every tile mutant caught.
+ `compile_with`, `schedule`, `execute_from`, `PlannedGate::{new, is_identity,
+ is_well_formed}` and `TILE_BITS_MAX`; `PLAN_REVISION` is 2. A tiled plan applies each
+ maximal run of consecutive gates that act inside a tile (diagonal gates on any target,
+ others with their target inside the tile) to tiles of up to 1024 amplitudes, in
+ op-log order, and leaves out exact identities only (circuit/1's `P(19..=32)` is
+ `2^30 − 1`, not the identity, and stays). The CPU executor mirrors a tile kernel's
+ index arithmetic and equals the per-gate program on any words; every configuration
+ certifies with the lanes32 pins. A program whose gate's `diag` disagrees with its
+ words, or whose op index is `u32::MAX`, is refused by the executor and by the member.
+ `wgsl::TILE_SHADER` (`lanes32.wgsl` followed by `lanes32_tile.wgsl`, entry point
+ `apply_tile`: one workgroup of 256 per tile, the tile updated in place in the state
+ planes, a storage barrier between gates, the tile size in the run record, never in
+ the text; a run record whose tiles or grid do not cover the state exactly once is
+ refused) has its own id (`tile_implementation_id`), records (`tile_gate_record`,
+ `tile_run_record`), grid (`tile_grid`) and mutants (`TILE_MUTANTS`, with the
+ detectors `Race` and `Malformed`, `race_witness`, `malformed_tile_run`).
+ `WebGpuMember::tiled()` gives the same device running it, probed and certified on its
+ own; the member splits a run into dispatches of at most `SUBMIT_BUDGET` pair-updates.
+ Discovery opens per-gate members, which are the fallback. Given both implementations'
+ entries for one member and no energy figures, `place` breaks the tie by
+ implementation name and places the tile implementation (`wgsl-lanes32-tile/…` sorts
+ first). The checker holds `TILE_SHADER` to its own rules, barrier placement and
+ barrier uniformity included, and its SPIR-V to the validator; the member's device
+ tests, the member script and the browser leg require the four pinned digests under
+ both implementations and every tile mutant caught. `tests/fabric_expected.rs` (not
+ in the package) holds `quantum-conformance/fabric/expected.json` to the crate.
- **The member in a browser.** wai-web's feature `quantum_webgpu` binds the member for JS
(`quantum_webgpu_discover`, `QuantumWebGpu`: `certify`, `statevector_hash_hex`) in a
bundle of its own (`--out-name wai_quantum_webgpu`); the default build and its bundle
diff --git a/wai/quantum-conformance/fabric/expected.json b/wai/quantum-conformance/fabric/expected.json
index f336a983e..c45501d5f 100644
--- a/wai/quantum-conformance/fabric/expected.json
+++ b/wai/quantum-conformance/fabric/expected.json
@@ -1,6 +1,6 @@
{
"implementation": "wgsl-lanes32/dc8e19f61e0e87ab",
- "tile_implementation": "wgsl-lanes32-tile/de6fb388454a0144",
+ "tile_implementation": "wgsl-lanes32-tile/54e443520c020013",
"certificates": [
{
"gateset": 1,
diff --git a/wai/tools/quantum-wgsl-check/src/main.rs b/wai/tools/quantum-wgsl-check/src/main.rs
index e25d0b99c..641f3b697 100644
--- a/wai/tools/quantum-wgsl-check/src/main.rs
+++ b/wai/tools/quantum-wgsl-check/src/main.rs
@@ -39,9 +39,16 @@
//! `apply_tile`, placed where the uniformity argument puts it: at the top
//! level of the entry point, or at the top level of the body of a `for`
//! loop at the top level, a loop with no `break` but its condition's and no
-//! `continue` or `return`. (Naga does not check that a barrier is in
-//! uniform control flow; this checks the placement the argument relies on,
-//! and a browser's WGSL compiler checks the uniformity itself.)
+//! `continue` or `return`. And every barrier is in uniform control flow:
+//! no condition of an `if`, `switch` or loop that encloses it, and no
+//! `return`, `break` or `continue` on a path to it under such a condition,
+//! depends on the invocation (its local or global id, or anything but the
+//! workgroup id, the number of workgroups and the workgroup size among the
+//! built-ins) or on memory other invocations write (read-write storage,
+//! workgroup memory, atomics), directly or through a variable or a call.
+//! Uniform buffers, read-only storage and buffer lengths are uniform. (Naga
+//! does not check barrier uniformity; this is the same analysis in the
+//! small, and a browser's WGSL compiler checks it too.)
//! 7. The module translates to MSL 2.4, to SPIR-V, and to HLSL shader model
//! 5.1, the model the DX12 member compiles for.
//! 8. Every mutant (`wgsl::MUTANTS` for `SHADER`, `wgsl::TILE_MUTANTS` for
@@ -50,13 +57,13 @@
//!
//! The last line of a passing run is `quantum-wgsl-check: ok`.
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use naga::back::{hlsl, msl, spv};
use naga::valid::{Capabilities, ModuleInfo, ValidationFlags, Validator};
-use naga::{AddressSpace, Arena, AtomicFunction, Barrier, BinaryOperator, Block, Expression, Handle, Literal, Module, Scalar, ScalarKind, ShaderStage, Statement, StorageAccess, TypeInner};
+use naga::{AddressSpace, Arena, AtomicFunction, Barrier, BinaryOperator, Binding, Block, BuiltIn, Expression, Function, Handle, Literal, Module, Scalar, ScalarKind, ShaderStage, Statement, StorageAccess, TypeInner};
use wai_quantum::quantum_fabric::wgsl::{self, Mutant};
const NAME: &str = "quantum-wgsl-check";
@@ -183,7 +190,11 @@ fn run(rules: &Rules, own: &str, file: Option<&Path>, out: Option<&Path>) -> Che
}
match rules.barriers_in {
None => println!("{NAME}: {label}: 6. atomics are atomicOr and atomicMin only; no barrier"),
- Some((ep, kind)) => println!("{NAME}: {label}: 6. atomics are atomicOr and atomicMin only; {barriers} barrier(s) of {kind:?}, all in {ep} at the top level of its body or of a top-level for loop without break, continue or return"),
+ Some((ep, kind)) => {
+ let e = module.entry_points.iter().find(|e| e.name == ep).ok_or_else(|| at(format!("6. no entry point {ep}")))?;
+ uniform_barriers(&module, &e.function, ep).map_err(at)?;
+ println!("{NAME}: {label}: 6. atomics are atomicOr and atomicMin only; {barriers} barrier(s) of {kind:?}, all in {ep} at the top level of its body or of a top-level for loop without break, continue or return, and in uniform control flow");
+ }
}
translate(rules, &module, &info, out).map_err(at)?;
mutants(rules, own, &text, file.is_some()).map_err(at)?;
@@ -459,6 +470,241 @@ fn statements(block: &Block, name: &str, barrier_kind: Option, depth: D
Ok(())
}
+/// Whether a global's contents can differ between invocations of one
+/// workgroup during a dispatch: memory some invocation may write.
+fn varying_global(module: &Module, g: Handle) -> bool {
+ match module.global_variables[g].space {
+ AddressSpace::Uniform | AddressSpace::Handle | AddressSpace::Immediate => false,
+ AddressSpace::Storage { access } => access.contains(StorageAccess::STORE),
+ _ => true,
+ }
+}
+
+/// Built-ins with one value for a whole workgroup.
+fn uniform_builtin(b: &Binding) -> bool {
+ matches!(b, Binding::BuiltIn(BuiltIn::WorkGroupId | BuiltIn::NumWorkGroups | BuiltIn::WorkGroupSize))
+}
+
+/// The functions whose result can vary between invocations whatever their
+/// arguments: they read varying memory, use an atomic, or call one that does.
+fn varying_functions(module: &Module) -> HashSet> {
+ let mut varying = HashSet::new();
+ loop {
+ let before = varying.len();
+ for (h, f) in module.functions.iter() {
+ let reads = f.expressions.iter().any(|(_, e)| match *e {
+ Expression::GlobalVariable(g) => varying_global(module, g),
+ Expression::AtomicResult { .. } => true,
+ _ => false,
+ });
+ if reads || calls(&f.body).iter().any(|c| varying.contains(c)) {
+ varying.insert(h);
+ }
+ }
+ if varying.len() == before {
+ return varying;
+ }
+ }
+}
+
+/// The functions a block calls.
+fn calls(block: &Block) -> Vec> {
+ let mut out = Vec::new();
+ for s in block.iter() {
+ match s {
+ Statement::Call { function, .. } => out.push(*function),
+ Statement::Block(b) => out.extend(calls(b)),
+ Statement::If { accept, reject, .. } => {
+ out.extend(calls(accept));
+ out.extend(calls(reject));
+ }
+ Statement::Switch { cases, .. } => cases.iter().for_each(|c| out.extend(calls(&c.body))),
+ Statement::Loop { body, continuing, .. } => {
+ out.extend(calls(body));
+ out.extend(calls(continuing));
+ }
+ _ => {}
+ }
+ }
+ out
+}
+
+/// How control leaves a block under a varying condition.
+#[derive(Clone, Copy, Default)]
+struct Exits {
+ /// A `return` some invocations take and others do not.
+ returns: bool,
+ /// A `break` or `continue` some invocations take and others do not.
+ breaks: bool,
+}
+
+/// The uniformity analysis of one function, in the small: which values and
+/// which control flow can differ between the invocations of a workgroup.
+struct Uniformity<'a> {
+ module: &'a Module,
+ f: &'a Function,
+ name: &'a str,
+ varying_fns: HashSet>,
+ /// Varying arguments (by position).
+ args: Vec,
+ /// Varying local variables (by handle index): stored a varying value, or
+ /// stored under varying control.
+ vars: Vec,
+ /// Call results that vary.
+ results: HashSet>,
+ changed: bool,
+ report: bool,
+ errors: BTreeSet,
+}
+
+impl Uniformity<'_> {
+ fn varies(&self, h: Handle) -> bool {
+ match self.f.expressions[h] {
+ Expression::Literal(_) | Expression::Constant(_) | Expression::Override(_) | Expression::ZeroValue(_) => false,
+ // A buffer's length is the same for every invocation.
+ Expression::ArrayLength(_) => false,
+ Expression::FunctionArgument(i) => self.args[i as usize],
+ // Pointers: what they point at matters when it is loaded.
+ Expression::GlobalVariable(_) | Expression::LocalVariable(_) => false,
+ Expression::Load { pointer } => self.varies_at(pointer),
+ Expression::Access { base, index } => self.varies(base) || self.varies(index),
+ Expression::AccessIndex { base, .. } => self.varies(base),
+ Expression::Splat { value, .. } => self.varies(value),
+ Expression::Swizzle { vector, .. } => self.varies(vector),
+ Expression::Compose { ref components, .. } => components.iter().any(|c| self.varies(*c)),
+ Expression::Unary { expr, .. } => self.varies(expr),
+ Expression::Binary { left, right, .. } => self.varies(left) || self.varies(right),
+ Expression::Select { condition, accept, reject } => self.varies(condition) || self.varies(accept) || self.varies(reject),
+ Expression::Relational { argument, .. } => self.varies(argument),
+ Expression::CallResult(_) => self.results.contains(&h),
+ // Atomics, and anything this analysis does not know: assumed to vary.
+ _ => true,
+ }
+ }
+
+ /// Whether the memory a pointer expression names can vary: its root's
+ /// contents, or any index on the way.
+ fn varies_at(&self, pointer: Handle) -> bool {
+ match self.f.expressions[pointer] {
+ Expression::GlobalVariable(g) => varying_global(self.module, g),
+ Expression::LocalVariable(v) => self.vars[v.index()],
+ Expression::Access { base, index } => self.varies(index) || self.varies_at(base),
+ Expression::AccessIndex { base, .. } => self.varies_at(base),
+ Expression::FunctionArgument(i) => self.args[i as usize],
+ _ => true,
+ }
+ }
+
+ /// The local variable a pointer expression stores into, if any.
+ fn local_root(&self, pointer: Handle) -> Option {
+ match self.f.expressions[pointer] {
+ Expression::LocalVariable(v) => Some(v.index()),
+ Expression::Access { base, .. } | Expression::AccessIndex { base, .. } => self.local_root(base),
+ _ => None,
+ }
+ }
+
+ fn mark_var(&mut self, v: usize) {
+ if !self.vars[v] {
+ self.vars[v] = true;
+ self.changed = true;
+ }
+ }
+
+ /// Walks `block` with its control flow varying or not; returns how it
+ /// leaves under varying conditions.
+ fn block(&mut self, block: &Block, mut varying: bool) -> Exits {
+ let mut exits = Exits::default();
+ for s in block.iter() {
+ match s {
+ Statement::Store { pointer, value } => {
+ if let Some(v) = self.local_root(*pointer)
+ && (varying || self.varies(*value) || self.varies_at(*pointer))
+ {
+ self.mark_var(v);
+ }
+ }
+ Statement::Call { function, arguments, result: Some(r) } => {
+ if (self.varying_fns.contains(function) || arguments.iter().any(|a| self.varies(*a))) && self.results.insert(*r) {
+ self.changed = true;
+ }
+ }
+ Statement::ControlBarrier(_) | Statement::MemoryBarrier(_) => {
+ if varying && self.report {
+ self.errors.insert(format!("6. in `{}`: a barrier in control flow that can differ between the invocations of a workgroup", self.name));
+ }
+ }
+ Statement::If { condition, accept, reject } => {
+ let c = varying || self.varies(*condition);
+ let (a, r) = (self.block(accept, c), self.block(reject, c));
+ exits.returns |= a.returns || r.returns;
+ exits.breaks |= a.breaks || r.breaks;
+ varying |= a.returns || a.breaks || r.returns || r.breaks;
+ }
+ Statement::Switch { selector, cases } => {
+ let c = varying || self.varies(*selector);
+ let mut any = false;
+ for case in cases {
+ let e = self.block(&case.body, c);
+ // A `break` leaves the switch; after it, control may still differ.
+ exits.returns |= e.returns;
+ any |= e.returns || e.breaks || (c && !case.body.is_empty());
+ }
+ varying |= any && c;
+ }
+ Statement::Loop { body, continuing, break_if } => {
+ let pass = |u: &mut Self, v: bool| {
+ let (b, c) = (u.block(body, v), u.block(continuing, v));
+ let leaves = break_if.is_some_and(|e| u.varies(e));
+ (b.returns || c.returns, b.breaks || c.breaks || leaves)
+ };
+ let (mut returns, mut leaves) = pass(self, varying);
+ if (returns || leaves) && !varying {
+ // Some invocations leave the loop, or an iteration,
+ // where others do not: the whole loop varies.
+ (returns, leaves) = pass(self, true);
+ }
+ exits.returns |= returns;
+ varying |= returns || leaves;
+ }
+ Statement::Block(b) => {
+ let e = self.block(b, varying);
+ exits.returns |= e.returns;
+ exits.breaks |= e.breaks;
+ varying |= e.returns || e.breaks;
+ }
+ Statement::Return { .. } => exits.returns |= varying,
+ Statement::Break | Statement::Continue => exits.breaks |= varying,
+ _ => {}
+ }
+ }
+ exits
+ }
+}
+
+/// Step 6, uniformity: every barrier of the entry point `name` is in control
+/// flow that is the same for every invocation of a workgroup.
+fn uniform_barriers(module: &Module, f: &Function, name: &str) -> Check {
+ let args = f.arguments.iter().map(|a| !a.binding.as_ref().is_some_and(uniform_builtin)).collect();
+ let mut u = Uniformity { module, f, name, varying_fns: varying_functions(module), args, vars: vec![false; f.local_variables.len()], results: HashSet::new(), changed: true, report: false, errors: BTreeSet::new() };
+ for (h, v) in f.local_variables.iter() {
+ if v.init.is_some_and(|e| u.varies(e)) {
+ u.vars[h.index()] = true;
+ }
+ }
+ // Varying is monotone: walk until nothing new varies, then once more to report.
+ while u.changed {
+ u.changed = false;
+ u.block(&f.body, false);
+ }
+ u.report = true;
+ u.block(&f.body, false);
+ match u.errors.into_iter().next() {
+ Some(e) => Err(e),
+ None => Ok(()),
+ }
+}
+
/// Step 7.
fn translate(rules: &Rules, module: &Module, info: &ModuleInfo, out: Option<&Path>) -> Check {
let label = rules.label;
diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml
index 0ef72a800..2677358f7 100644
--- a/wai/wai-quantum/Cargo.toml
+++ b/wai/wai-quantum/Cargo.toml
@@ -11,6 +11,9 @@ documentation = "https://docs.rs/wai-quantum"
keywords = ["quantum", "simulation", "wasm", "deterministic", "qec"]
categories = ["science", "simulation", "wasm", "mathematics"]
readme = "README.md"
+# Holds a file outside this package (quantum-conformance/fabric/expected.json) to
+# the crate; it runs in the repository only.
+exclude = ["tests/fabric_expected.rs"]
# docs.rs builds with DEFAULT features only, which would document 3 of 21
# modules and hide the entire operational + ML layers. Document everything.
@@ -159,6 +162,10 @@ full = [
name = "webgpu_member"
required-features = ["quantum_fabric_webgpu"]
+[[test]]
+name = "fabric_expected"
+required-features = ["quantum_fabric"]
+
[[example]]
name = "qio_gset"
required-features = ["quantum_qio"]
diff --git a/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl b/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
index 828fa437c..ecb00e3ec 100644
--- a/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
+++ b/wai/wai-quantum/src/quantum_fabric/lanes32_tile.wgsl
@@ -9,11 +9,11 @@
// workgroup memory: the tile stays in re and im, which no other workgroup of
// the dispatch touches.
//
-// Uniformity: the early returns depend only on run (a uniform buffer) and
-// workgroup_id. The barrier sits at the top level of the r loop, whose bound
-// run.count is a uniform-buffer value; no break, continue or return occurs
-// inside that loop, and no branch that depends on a gate or an invocation
-// holds a barrier.
+// Uniformity: the early returns depend only on run (a uniform buffer),
+// workgroup_id, num_workgroups and the state planes' lengths. The barrier sits
+// at the top level of the r loop, whose bound run.count is a uniform-buffer
+// value; no break, continue or return occurs inside that loop, and no branch
+// that depends on a gate or an invocation holds a barrier.
struct TileGate {
row0: vec4, // m00.re, m00.im, m01.re, m01.im
@@ -45,18 +45,24 @@ const ZERO2: vec2 = vec2(0u, 0u);
@compute @workgroup_size(256)
fn apply_tile(@builtin(workgroup_id) wid: vec3,
+ @builtin(num_workgroups) groups: vec3,
@builtin(local_invocation_index) lid: u32) {
let size = run.size;
- // A malformed run record is reported and never run (uniform: run is a uniform buffer).
+ let span = run.tiles * size; // 2^n
+ // A malformed run record, or one whose tiles or grid do not cover the state
+ // exactly once, is reported and never run (uniform: run is a uniform buffer,
+ // num_workgroups a uniform built-in, and a buffer's length is the same for
+ // every invocation).
if (size == 0u || size > TILE_MAX || (size & (size - 1u)) != 0u || run.count == 0u
- || run.x_groups == 0u) {
+ || run.tiles == 0u || span != arrayLength(&re) || span != arrayLength(&im)
+ || run.x_groups == 0u || groups.x != run.x_groups || groups.z != 1u
+ || groups.x * groups.y < run.tiles) {
atomicOr(&report[0], 4u);
return;
}
let tile = wid.y * run.x_groups + wid.x;
if (tile >= run.tiles) { return; } // workgroup-uniform
let base = tile * size;
- let span = run.tiles * size; // 2^n
// A tile outside the state is reported, and nothing of it is touched.
let inside = base + size <= arrayLength(&re) && base + size <= arrayLength(&im);
if (!inside) { atomicOr(&report[0], 2u); }
diff --git a/wai/wai-quantum/src/quantum_fabric/plan.rs b/wai/wai-quantum/src/quantum_fabric/plan.rs
index 71d6114d5..382437839 100644
--- a/wai/wai-quantum/src/quantum_fabric/plan.rs
+++ b/wai/wai-quantum/src/quantum_fabric/plan.rs
@@ -98,7 +98,8 @@ impl Config {
}
/// Gates per tile run on `n_qubits`, at most: `run_max`, capped at
- /// `2^30 / 2^n` and at least one.
+ /// `2^30 / 2^n` and at least one. A member may split a run into several
+ /// dispatches to bound each one's work (splitting a run is exact).
pub fn run_cap(&self, n_qubits: u8) -> u32 {
let budget = if n_qubits >= 30 { 1 } else { (1u32 << 30) >> n_qubits };
self.run_max.min(budget).max(1)
@@ -122,6 +123,16 @@ impl PlannedGate {
PlannedGate { words, index, diag: words.m[2..6] == [0; 4] }
}
+ /// Whether a member may run the gate: `diag` says what the words say (a
+ /// tile kernel trusts it to act per amplitude), and the index is not
+ /// `u32::MAX`, which the tile kernel's report keeps for "no op". Every
+ /// gate [`PlannedGate::new`] and [`compile`] make is well formed:
+ /// `validate` holds an op-log's length, and so every index, below
+ /// `u32::MAX`.
+ pub fn is_well_formed(&self) -> bool {
+ self.diag == (self.words.m[2..6] == [0; 4]) && self.index != u32::MAX
+ }
+
/// Whether the words are exactly the identity's (whatever the target and
/// controls): the gate changes nothing and cannot leave 32 bits.
pub fn is_identity(&self) -> bool {
@@ -236,14 +247,15 @@ pub fn execute(p: &Program) -> LaneRun {
/// The program's steps on the CPU from `state` (which replaces the basis
/// state `p.start`): each [`Step::Gate`] with [`lanes::apply_words`], each
/// [`Step::Tile`] tile by tile with a tile kernel's own index arithmetic.
-/// `None` if `state` is not `2^n` lanes or the program is malformed: a step
-/// naming gates it does not hold, a tile size that is not a power of two of
-/// at most `2^TILE_BITS_MAX` and the state's length, an empty run, or words
-/// a kernel refuses (those [`lanes::apply_words`] refuses, and in a run a
-/// gate that neither is diagonal nor has its target's bit below the tile's
-/// size).
+/// `None` if `state` is not `2^n` lanes or the program is malformed: a gate
+/// that is not [`PlannedGate::is_well_formed`], a step naming gates it does
+/// not hold, a tile size that is not a power of two of at most
+/// `2^TILE_BITS_MAX` and the state's length, an empty run, or words a kernel
+/// refuses (those [`lanes::apply_words`] refuses, and in a run a gate that
+/// neither is diagonal nor has its target's bit below the tile's size). A
+/// member refuses the same programs.
pub fn execute_from(p: &Program, state: &[Lane]) -> Option {
- if p.n_qubits > MAX_QUBITS || state.len() != 1usize << p.n_qubits {
+ if p.n_qubits > MAX_QUBITS || state.len() != 1usize << p.n_qubits || !p.gates.iter().all(PlannedGate::is_well_formed) {
return None;
}
let mut amps = state.to_vec();
@@ -655,6 +667,24 @@ mod tests {
let p = schedule(3, 0, vec![refused_identity], &Config::TILED);
assert_eq!(p.gates.len(), 1);
assert_eq!(execute_from(&p, &state), None);
+ // A gate whose `diag` disagrees with its words: X planned as diagonal
+ // would act per amplitude, and wrongly; a diagonal gate planned as not
+ // diagonal is refused alike. In a run and alone.
+ let x0 = PlannedGate::new(lanes::gate_words(&Gate { base: BaseGate::X, controls: vec![], target: 0, param: 0 }, Gateset::V2), 0);
+ for g in [PlannedGate { diag: true, ..x0 }, PlannedGate { diag: false, ..z0 }] {
+ assert!(!g.is_well_formed());
+ for step in [Step::Tile { first: 0, count: 1, size: 8 }, Step::Gate { gate: 0 }] {
+ assert_eq!(execute_from(&program(vec![g], vec![step]), &state), None, "{g:?} {step:?}");
+ }
+ }
+ assert!(x0.is_well_formed() && z0.is_well_formed());
+ // The index the tile kernel keeps for "no op".
+ let last = PlannedGate { index: u32::MAX, ..z0 };
+ assert!(!last.is_well_formed());
+ for step in [Step::Tile { first: 0, count: 1, size: 8 }, Step::Gate { gate: 0 }] {
+ assert_eq!(execute_from(&program(vec![last], vec![step]), &state), None);
+ }
+ assert!(execute_from(&program(vec![PlannedGate { index: u32::MAX - 1, ..z0 }], vec![Step::Tile { first: 0, count: 1, size: 8 }]), &state).is_some());
// A state of the wrong length.
assert_eq!(execute_from(&program(vec![z0], vec![Step::Gate { gate: 0 }]), &state[..4]), None);
}
diff --git a/wai/wai-quantum/src/quantum_fabric/wgsl.rs b/wai/wai-quantum/src/quantum_fabric/wgsl.rs
index 7b6da73ff..fd794a2f6 100644
--- a/wai/wai-quantum/src/quantum_fabric/wgsl.rs
+++ b/wai/wai-quantum/src/quantum_fabric/wgsl.rs
@@ -100,8 +100,12 @@
//! it applies every gate of the run to the tile in place, in op-log order. A
//! run of gates is one dispatch instead of one per gate. The steps of a tiled
//! program that are single gates run on the same module's `apply`. It is its
-//! own implementation ([`tile_implementation_id`]), certified on its own; the
-//! per-gate kernel stays the member's default and its fallback.
+//! own implementation ([`tile_implementation_id`]), certified on its own. A
+//! member runs the per-gate kernel unless its caller asks for tile runs, and
+//! falls back to it; [`place`](super::place), with both implementations'
+//! entries for one member and no energy figures, breaks the tie by
+//! implementation name and places the tile implementation
+//! (`wgsl-lanes32-tile/…` sorts before `wgsl-lanes32/…`).
//!
//! - **The same function.** `apply_tile` computes every output with `row`, the
//! per-gate kernel's exact 64-bit row, and stores its low words: for a pair,
@@ -116,11 +120,16 @@
//! belongs to one invocation; a storage barrier (workgroup execution scope,
//! storage memory) separates each gate from the next, so a gate reads what
//! the gate before wrote; tiles are disjoint, one workgroup each, and the
-//! dispatch ends before the next step reads the state. A tile outside the
-//! state is reported (bit 2) and not touched; a record outside the gate
-//! buffer reads as malformed, never out of bounds.
+//! dispatch ends before the next step reads the state. A run record is
+//! refused (bit 4) and nothing is touched unless its size is a power of two
+//! of at most 1024, its run holds a gate, its tiles cover each state plane
+//! exactly, and its grid (`num_workgroups`, one deep) holds every tile once
+//! with `x_groups` workgroups to a row. A tile outside the state is reported
+//! (bit 2) and not touched; a record outside the gate buffer reads as
+//! malformed, never out of bounds.
//! - **A uniform barrier.** The early returns depend only on the run record (a
-//! uniform buffer) and the workgroup id. The barrier sits at the top level
+//! uniform buffer), the workgroup id, the number of workgroups and the
+//! state planes' lengths. The barrier sits at the top level
//! of the loop over the run's gates, whose bound is in the uniform record;
//! that loop holds no `break`, `continue` or `return`, and no branch that
//! depends on a gate or an invocation holds a barrier. Naga does not check
@@ -720,6 +729,30 @@ mod tests {
}
}
+ /// With both implementations' entries for one member and no energy
+ /// figures, placement breaks the tie by implementation name: the tile
+ /// implementation sorts first and is placed, as the documentation says.
+ #[test]
+ fn placement_breaks_an_implementation_tie_by_name() {
+ assert!(tile_implementation_id() < implementation_id());
+ let id = hardware().member_id();
+ for gs in Gateset::ALL {
+ let kernel = apply_kernel(gs);
+ let entry = |implementation: String| {
+ let certificate = Certificate { kernel: kernel.into(), implementation: implementation.clone(), member: id.clone(), generator: FABRIC_DEFAULT.generator(gs), parity: Parity::BitExact, cases: 224, corpus: [7; 32] };
+ KernelEntry { kernel: kernel.into(), implementation, certificate, energy: None }
+ };
+ for kernels in [vec![entry(implementation_id()), entry(tile_implementation_id())], vec![entry(tile_implementation_id()), entry(implementation_id())]] {
+ let m = Member { id: id.clone(), class: FabricClass::Gpu, presence: Presence::Detected, kernels, detail: String::new() };
+ let (_, k) = place(std::slice::from_ref(&m), kernel, &Parity::BitExact).unwrap();
+ assert_eq!(k.implementation, tile_implementation_id(), "{gs:?}");
+ }
+ // Offered alone, the per-gate entry is placed.
+ let m = Member { id: id.clone(), class: FabricClass::Gpu, presence: Presence::Detected, kernels: vec![entry(implementation_id())], detail: String::new() };
+ assert_eq!(place(std::slice::from_ref(&m), kernel, &Parity::BitExact).unwrap().1.implementation, implementation_id());
+ }
+ }
+
#[test]
fn mutants_apply_once() {
let names: Vec<&str> = MUTANTS.iter().map(|m| m.name).collect();
@@ -851,7 +884,7 @@ mod tests {
/// documentation, to the host layout ([`HOST_LAYOUT`]) or to the meaning
/// of a program ([`PLAN_REVISION`]): every certificate of the old id must
/// then be earned again.
- const PINNED_TILE_IMPLEMENTATION: &str = "wgsl-lanes32-tile/de6fb388454a0144";
+ const PINNED_TILE_IMPLEMENTATION: &str = "wgsl-lanes32-tile/54e443520c020013";
#[test]
fn tile_implementation_id_is_pinned() {
diff --git a/wai/wai-quantum/src/quantum_fabric_webgpu.rs b/wai/wai-quantum/src/quantum_fabric_webgpu.rs
index de383b475..54bf85cf4 100644
--- a/wai/wai-quantum/src/quantum_fabric_webgpu.rs
+++ b/wai/wai-quantum/src/quantum_fabric_webgpu.rs
@@ -22,8 +22,14 @@
//! with [`Config::TILED`] and runs each run of gates that act inside a tile
//! as one dispatch, one workgroup per tile; its run records go in a second
//! uniform ring and its gates in a storage buffer, per submission. It is
-//! probed and certified on its own; the per-gate member stays the default
-//! and the fallback.
+//! probed and certified on its own. Discovery opens per-gate members; a tile
+//! member exists only when a caller asks for one, and the per-gate kernel
+//! is the one to fall back to. Placement has no preference of its own:
+//! given both implementations' entries for one member and no energy
+//! figures, [`place`](crate::quantum_fabric::place) breaks the tie by
+//! implementation name, and `wgsl-lanes32-tile/…` sorts before
+//! `wgsl-lanes32/…`, so the tile implementation is placed; a caller who
+//! wants the per-gate kernel offers only its entries.
//! - **Certification** ([`WebGpuMember::certify`], [`WebGpuMember::member`]):
//! the member's own run of a corpus, folded case by case into a
//! [`Certification`]. The corpus digest covers inputs and outputs only, so a
@@ -839,7 +845,8 @@ impl WebGpuMember {
/// with [`Config::TILED`], and implementation
/// [`wgsl::tile_implementation_id`]. A tile member earns its own
/// certificates ([`Self::certify`], [`Self::member`]); this member is
- /// unchanged and stays the default.
+ /// unchanged. See the module documentation for how placement orders the
+ /// two implementations.
pub async fn tiled(&self) -> Result {
self.alive()?;
let pipelines = kernel_pipelines(&self.device, Cow::Borrowed(wgsl::TILE_SHADER), true).await?;
@@ -929,22 +936,34 @@ impl WebGpuMember {
}
/// Runs `steps` over `gates` on `2^n` lanes from `init`, in place, in
- /// order: one dispatch per step, a gate step over the whole state and a
- /// tile step one workgroup per tile. A step the host cannot lay out (a
- /// gate or range it does not hold, a tile size of zero or above the
- /// state's length, a tile step on a member without the tile kernel, a
- /// grid past the per-dimension limit) is refused as
- /// [`WebGpuError::Internal`] before anything runs; the kernels refuse the
- /// rest with report bit 4.
+ /// order: one dispatch per gate step, over the whole state, and one or
+ /// more per tile step, one workgroup per tile. A tile step whose gates
+ /// would make one dispatch of more than [`SUBMIT_BUDGET`] pair-updates is
+ /// split into dispatches of fewer gates each (splitting a run is exact),
+ /// so that every submission stays within the budget. A program the host
+ /// cannot lay out (a gate that is not
+ /// [`PlannedGate::is_well_formed`], a gate or range it does not hold, a
+ /// tile size of zero or above the state's length, a tile step on a member
+ /// without the tile kernel, a grid past the per-dimension limit) is
+ /// refused as [`WebGpuError::Internal`] before anything runs; the kernels
+ /// refuse the rest with report bit 4.
async fn run(&self, n: u8, init: Init<'_>, gates: &[PlannedGate], steps: &[Step]) -> Result {
self.alive()?;
let len = 1usize << n;
let plane = 4u64 << n;
let grid = wgsl::grid(1u32 << (n - 1), self.max_x);
let ring_size = u64::from(self.ring_records) * self.stride;
+ let pairs = 1u64 << (n - 1);
- // What the host lays out: every step in range, and the tile kernel
- // for tile steps.
+ // What the host lays out: well-formed gates, every step in range, and
+ // the tile kernel for tile steps, each split to at most `per_dispatch`
+ // gates.
+ if !gates.iter().all(PlannedGate::is_well_formed) {
+ return Err(WebGpuError::Internal(4));
+ }
+ let per_dispatch = (SUBMIT_BUDGET / pairs).clamp(1, u64::from(u32::MAX)) as u32;
+ let mut laid: Vec = Vec::new();
+ laid.try_reserve(steps.len()).map_err(|e| WebGpuError::Memory(e.to_string()))?;
let mut longest = 0usize;
for step in steps {
match *step {
@@ -952,6 +971,7 @@ impl WebGpuMember {
if gate as usize >= gates.len() {
return Err(WebGpuError::Internal(4));
}
+ laid.push(*step);
}
Step::Tile { first, count, size } => {
let end = (first as usize).checked_add(count as usize);
@@ -959,10 +979,21 @@ impl WebGpuMember {
if self.pipelines.tile.is_none() || end.is_none_or(|e| e > gates.len()) || tiles == 0 || self.tile_grid(tiles).y > 65535 {
return Err(WebGpuError::Internal(4));
}
- longest = longest.max(count as usize);
+ if count == 0 {
+ // The kernel refuses an empty run itself (bit 4).
+ laid.push(*step);
+ }
+ let mut at = first;
+ while at < first + count {
+ let part = per_dispatch.min(first + count - at);
+ laid.push(Step::Tile { first: at, count: part, size });
+ longest = longest.max(part as usize);
+ at += part;
+ }
}
}
}
+ let steps = laid.as_slice();
let tile_steps = steps.iter().any(|s| matches!(s, Step::Tile { .. }));
// Tile gates per submission: a ring's worth, or the longest run.
let tile_capacity = (self.ring_records as usize).max(longest);
@@ -1033,10 +1064,10 @@ impl WebGpuMember {
// The steps, a chunk per submission, each waited on natively before
// the next, so that POLL_TIMEOUT bounds one chunk's GPU time. A chunk
- // holds at least one step, and more while its pair-updates stay within
+ // holds at least one step (at most SUBMIT_BUDGET pair-updates, as laid
+ // out above), and more while its pair-updates stay within
// SUBMIT_BUDGET, each ring holds its records and the tile gate buffer
// holds the runs' gates.
- let pairs = 1u64 << (n - 1);
let ring_records = self.ring_records as usize;
let stride = self.stride as usize;
let mut next = 0;
diff --git a/wai/wai-quantum/tests/fabric_expected.rs b/wai/wai-quantum/tests/fabric_expected.rs
new file mode 100644
index 000000000..2d52ae43e
--- /dev/null
+++ b/wai/wai-quantum/tests/fabric_expected.rs
@@ -0,0 +1,61 @@
+//! `quantum-conformance/fabric/expected.json` is what the browser leg of the
+//! fabric's WebGPU member holds a page to (`browser_webgpu.mjs`). This holds
+//! the file to the crate: it must be exactly the four certificates of the
+//! fabric's pinned corpora (both gatesets; random-v1 and fabric-v1 with their
+//! seeds, sizes and wide circuits; the corpus digests the CPU computes) and
+//! the two implementation ids, so that editing the file alone cannot weaken
+//! the leg, for instance by giving fabric-v1 random-v1's digest and so never
+//! running a circuit wider than one tile. A missing or unreadable file fails.
+//!
+//! The file lives outside this package, so this test is excluded from it.
+
+use std::path::Path;
+use serde_json::{Value, json};
+use wai_quantum::quantum::Gateset;
+use wai_quantum::quantum_fabric::{Corpus, FABRIC_DEFAULT, apply_kernel, lanes, wgsl};
+
+fn hex(b: &[u8]) -> String {
+ b.iter().map(|x| format!("{x:02x}")).collect()
+}
+
+#[test]
+fn the_browser_legs_pins_are_the_crates() {
+ let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance/fabric/expected.json");
+ let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
+ let file: Value = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
+ let Corpus::Fabric { seed, cases, wide } = FABRIC_DEFAULT else { panic!("FABRIC_DEFAULT is a fabric corpus") };
+ let random = Corpus::Random { seed, cases };
+ let mut certificates = Vec::new();
+ for (corpus, wide) in [(random, 0), (FABRIC_DEFAULT, wide)] {
+ for (number, gs) in [(1, Gateset::V1), (2, Gateset::V2)] {
+ let cert = lanes::certify_corpus(gs, "local/test", corpus);
+ certificates.push(json!({
+ "gateset": number,
+ "kernel": apply_kernel(gs),
+ "corpus": corpus.name(),
+ "seed": seed,
+ "cases": cases,
+ "wide": wide,
+ "cases_compared": cert.cases,
+ "corpus_hex": hex(&cert.corpus),
+ }));
+ }
+ }
+ let want = json!({
+ "implementation": wgsl::implementation_id(),
+ "tile_implementation": wgsl::tile_implementation_id(),
+ "certificates": certificates,
+ });
+ assert_eq!(file, want, "{} is not the crate's pins; the crate's are:\n{}", path.display(), serde_json::to_string_pretty(&want).unwrap());
+ // The four digests are the pinned ones, and fabric-v1's are not random-v1's.
+ let digests: Vec<&str> = file["certificates"].as_array().unwrap().iter().map(|c| c["corpus_hex"].as_str().unwrap()).collect();
+ assert_eq!(
+ digests,
+ [
+ "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36",
+ "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2",
+ "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7",
+ "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809",
+ ]
+ );
+}
diff --git a/wai/wai-quantum/tests/webgpu_member.rs b/wai/wai-quantum/tests/webgpu_member.rs
index 84ba6b9c3..743dcb91f 100644
--- a/wai/wai-quantum/tests/webgpu_member.rs
+++ b/wai/wai-quantum/tests/webgpu_member.rs
@@ -706,6 +706,76 @@ fn g12_tiles_match_per_gate() {
assert!(hashes.iter().all(|h| *h == native), "{}: {hashes:?}, native {native}", m.key());
report(&format!("webgpu tile repeated: key={} 5 runs, one hash", m.key()));
+ // A start other than |0…0⟩, reduced modulo 2^n as the native simulator does.
+ for n in [12u8, 16] {
+ let c = random_circuit(n, 200, 0x7374_6172 + u64::from(n), Gateset::V2);
+ for start in [5usize, (1 << n) + 5] {
+ let native = c.simulate_from(start).unwrap().statevector_hash();
+ assert_eq!(hex(&blocking::simulate_from(&tiled, &c, start).unwrap().statevector_hash()), hex(&native), "{} tiled n = {n} start {start}", 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. The filler is circuit/1's P(19),
+ // which is not the identity, so the tile planner keeps every gate; on
+ // two qubits it touches only amplitudes that are zero.
+ let p19 = lanes::gate_words(&Gate { base: BaseGate::P, controls: vec![], target: 0, param: 19 }, Gateset::V1);
+ let z1 = lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 1, param: 0 }, Gateset::V2);
+ let state: [Lane; 4] = [[0, 0], [0, 0], [i32::MIN, 0], [0, 0]];
+ let gates: Vec = (0..5000).map(|k| if k == 4100 || k == 4500 { z1 } else { p19 }).collect();
+ let want = lanes::run_words(&state, &gates).expect("valid shapes");
+ assert_eq!(want.out_of_range, Some(4100));
+ for member in [tiled.clone(), tiled.with_ring_records(3)] {
+ assert_eq!(run(member.run_lanes(&state, &gates)), want, "{} tiled, first out of range at 4100, {member:?}", m.key());
+ }
+ // A run longer than one dispatch may hold: at 20 qubits a dispatch of
+ // SUBMIT_BUDGET pair-updates is 512 gates, and a run may hold 1024; the
+ // member splits it, and the state is the per-gate member's.
+ let mut s = 0x7370_6c69u64;
+ let diagonal = [BaseGate::Z, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
+ let mut long_run = Circuit::with_gateset(20, Gateset::V2);
+ for _ in 0..600 {
+ let base = diagonal[(next(&mut s) % 6) as usize];
+ let target = (next(&mut s) % 20) as u8;
+ let param = if base == BaseGate::P { 1 + (next(&mut s) % 32) as u16 } else { 0 };
+ long_run.ops.push(Gate { base, controls: vec![], target, param });
+ }
+ let planned = plan::compile_with(&long_run, 0, &Config::TILED).unwrap();
+ assert_eq!(planned.steps, [Step::Tile { first: 0, count: 600, size: 1024 }]);
+ assert!(600 > (wai_quantum::quantum_fabric_webgpu::SUBMIT_BUDGET >> 19));
+ let per_gate = blocking::simulate_from(m, &long_run, 0).unwrap().statevector_hash();
+ assert_eq!(hex(&blocking::simulate_from(&tiled, &long_run, 0).unwrap().statevector_hash()), hex(&per_gate), "{} a split run", m.key());
+ // Planned gates a member may not run are refused before anything
+ // runs, by both implementations: a `diag` that disagrees with the
+ // words, and the op index the tile kernel keeps for "no op".
+ let x0 = PlannedGate::new(lanes::gate_words(&Gate { base: BaseGate::X, controls: vec![], target: 0, param: 0 }, Gateset::V2), 0);
+ for g in [PlannedGate { diag: true, ..x0 }, PlannedGate { index: u32::MAX, ..x0 }] {
+ for (member, step) in [(m, Step::Gate { gate: 0 }), (&tiled, Step::Gate { gate: 0 }), (&tiled, Step::Tile { first: 0, count: 1, size: 4 })] {
+ let p = Program { n_qubits: 2, start: 0, gates: vec![g], steps: vec![step] };
+ assert_eq!(attempt(member.run_program(&p)).unwrap_err(), WebGpuError::Internal(4), "{} {g:?} {step:?}", m.key());
+ }
+ }
+ report(&format!("webgpu tile edges: key={} ok", m.key()));
+
+ // The widest states, as g6 runs them per gate: a run cap of 16 gates
+ // and a grid of (8, 8192) tiles at 26 qubits.
+ if std::env::var("WAI_QUANTUM_WEBGPU_WIDE").is_ok_and(|v| v == "1") && m.class() == FabricClass::Gpu {
+ let mut sizes = vec![24u8];
+ if m.max_qubits() > 24 {
+ sizes.push(m.max_qubits());
+ }
+ 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 in [&qft, &random] {
+ let native = c.simulate().unwrap().statevector_hash();
+ let got = blocking::simulate_from(&tiled, c, 0).unwrap_or_else(|e| panic!("{} tiled n = {n}: {e}", m.key()));
+ assert_eq!(hex(&got.statevector_hash()), hex(&native), "{} tiled n = {n}", m.key());
+ }
+ report(&format!("webgpu tile wide: key={} n={n} matches native", m.key()));
+ }
+ }
+
// Timing, per-gate and tiled on the same circuits: printed, never
// asserted. The hashes must agree.
// On 10 qubits every gate fits one tile: runs of up to 1024 gates.
--
2.54.0 (Apple Git-157)
From 738a2cf3cbe608c08c0a488506175fd5a344a6cd Mon Sep 17 00:00:00 2001
From: Transaction Science
Date: Sun, 11 Oct 2026 23:34:28 -0400
Subject: [PATCH 4/4] wai-quantum 0.4.2: tile runs and the browser member, as
their own release
0.4.1 is published, so the tile implementation and the browser member go out as
0.4.2, additive. Their CHANGELOG entries move out of the 0.4.1 entry into one of
their own; the version line changes in the six lockfiles that resolve
wai-quantum.
Co-Authored-By: Claude Opus 5.5
---
wai/CHANGELOG.md | 80 +++++++++++++------------
wai/tools/quantum-wgsl-check/Cargo.lock | 2 +-
wai/wai-pack/Cargo.lock | 2 +-
wai/wai-quantum-serve/Cargo.lock | 2 +-
wai/wai-quantum/Cargo.lock | 2 +-
wai/wai-quantum/Cargo.toml | 2 +-
wai/wai-rs/Cargo.lock | 2 +-
wai/wai-web/Cargo.lock | 2 +-
8 files changed, 50 insertions(+), 44 deletions(-)
diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md
index 46550f6cb..668b5ead3 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -6,6 +6,49 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
+### wai-quantum 0.4.2: tile runs, and the WebGPU member in a browser
+
+Additive. The default features, `full`, the per-gate implementation and every existing pin
+and digest are unchanged.
+
+- **Tile runs: a second implementation, `wgsl-lanes32-tile/54e443520c020013`.**
+ `plan::Config` (`PER_GATE`, `TILED`; `tile_size`, `run_cap`), `plan::Step::Tile`,
+ `compile_with`, `schedule`, `execute_from`, `PlannedGate::{new, is_identity,
+ is_well_formed}` and `TILE_BITS_MAX`; `PLAN_REVISION` is 2. A tiled plan applies each
+ maximal run of consecutive gates that act inside a tile (diagonal gates on any target,
+ others with their target inside the tile) to tiles of up to 1024 amplitudes, in
+ op-log order, and leaves out exact identities only (circuit/1's `P(19..=32)` is
+ `2^30 − 1`, not the identity, and stays). The CPU executor mirrors a tile kernel's
+ index arithmetic and equals the per-gate program on any words; every configuration
+ certifies with the lanes32 pins. A program whose gate's `diag` disagrees with its
+ words, or whose op index is `u32::MAX`, is refused by the executor and by the member.
+ `wgsl::TILE_SHADER` (`lanes32.wgsl` followed by `lanes32_tile.wgsl`, entry point
+ `apply_tile`: one workgroup of 256 per tile, the tile updated in place in the state
+ planes, a storage barrier between gates, the tile size in the run record, never in
+ the text; a run record whose tiles or grid do not cover the state exactly once is
+ refused) has its own id (`tile_implementation_id`), records (`tile_gate_record`,
+ `tile_run_record`), grid (`tile_grid`) and mutants (`TILE_MUTANTS`, with the
+ detectors `Race` and `Malformed`, `race_witness`, `malformed_tile_run`).
+ `WebGpuMember::tiled()` gives the same device running it, probed and certified on its
+ own; the member splits a run into dispatches of at most `SUBMIT_BUDGET` pair-updates.
+ Discovery opens per-gate members, which are the fallback. Given both implementations'
+ entries for one member and no energy figures, `place` breaks the tie by
+ implementation name and places the tile implementation (`wgsl-lanes32-tile/…` sorts
+ first). The checker holds `TILE_SHADER` to its own rules, barrier placement and
+ barrier uniformity included, and its SPIR-V to the validator; the member's device
+ tests, the member script and the browser leg require the four pinned digests under
+ both implementations and every tile mutant caught. `tests/fabric_expected.rs` (not
+ in the package) holds `quantum-conformance/fabric/expected.json` to the crate.
+- **The member in a browser.** wai-web's feature `quantum_webgpu` binds the member for JS
+ (`quantum_webgpu_discover`, `QuantumWebGpu`: `certify`, `statevector_hash_hex`) in a
+ bundle of its own (`--out-name wai_quantum_webgpu`); the default build and its bundle
+ are unchanged. `demo/quantum_webgpu.html` runs the member in a worker, its kernel
+ compiled by the browser's own WGSL compiler. `quantum-conformance/browser_webgpu.mjs`
+ drives the page headless and requires the four certificates bit-exact with the pinned
+ digests (`quantum-conformance/fabric/expected.json`), every vector's statevector hash,
+ every reject file refused with its class, and a circuit wider than the member holds
+ refused. The `checks` job builds the bundle, and a `browser` job runs the page on macOS.
+
### wai-quantum 0.4.1: the fabric's WebGPU member
Additive. The default features, `full`, and every existing pin and digest are unchanged.
@@ -79,43 +122,6 @@ Additive. The default features, `full`, and every existing pin and digest are un
if the adapters found differ, if any test did not run, if a declared adapter lacks a
bit-exact certificate for each pinned digest, or if a shader mutant survives. The
`wasm32-wasip2` job requires the fabric's new unit tests by name.
-- **Tile runs: a second implementation, `wgsl-lanes32-tile/54e443520c020013`.**
- `plan::Config` (`PER_GATE`, `TILED`; `tile_size`, `run_cap`), `plan::Step::Tile`,
- `compile_with`, `schedule`, `execute_from`, `PlannedGate::{new, is_identity,
- is_well_formed}` and `TILE_BITS_MAX`; `PLAN_REVISION` is 2. A tiled plan applies each
- maximal run of consecutive gates that act inside a tile (diagonal gates on any target,
- others with their target inside the tile) to tiles of up to 1024 amplitudes, in
- op-log order, and leaves out exact identities only (circuit/1's `P(19..=32)` is
- `2^30 − 1`, not the identity, and stays). The CPU executor mirrors a tile kernel's
- index arithmetic and equals the per-gate program on any words; every configuration
- certifies with the lanes32 pins. A program whose gate's `diag` disagrees with its
- words, or whose op index is `u32::MAX`, is refused by the executor and by the member.
- `wgsl::TILE_SHADER` (`lanes32.wgsl` followed by `lanes32_tile.wgsl`, entry point
- `apply_tile`: one workgroup of 256 per tile, the tile updated in place in the state
- planes, a storage barrier between gates, the tile size in the run record, never in
- the text; a run record whose tiles or grid do not cover the state exactly once is
- refused) has its own id (`tile_implementation_id`), records (`tile_gate_record`,
- `tile_run_record`), grid (`tile_grid`) and mutants (`TILE_MUTANTS`, with the
- detectors `Race` and `Malformed`, `race_witness`, `malformed_tile_run`).
- `WebGpuMember::tiled()` gives the same device running it, probed and certified on its
- own; the member splits a run into dispatches of at most `SUBMIT_BUDGET` pair-updates.
- Discovery opens per-gate members, which are the fallback. Given both implementations'
- entries for one member and no energy figures, `place` breaks the tie by
- implementation name and places the tile implementation (`wgsl-lanes32-tile/…` sorts
- first). The checker holds `TILE_SHADER` to its own rules, barrier placement and
- barrier uniformity included, and its SPIR-V to the validator; the member's device
- tests, the member script and the browser leg require the four pinned digests under
- both implementations and every tile mutant caught. `tests/fabric_expected.rs` (not
- in the package) holds `quantum-conformance/fabric/expected.json` to the crate.
-- **The member in a browser.** wai-web's feature `quantum_webgpu` binds the member for JS
- (`quantum_webgpu_discover`, `QuantumWebGpu`: `certify`, `statevector_hash_hex`) in a
- bundle of its own (`--out-name wai_quantum_webgpu`); the default build and its bundle
- are unchanged. `demo/quantum_webgpu.html` runs the member in a worker, its kernel
- compiled by the browser's own WGSL compiler. `quantum-conformance/browser_webgpu.mjs`
- drives the page headless and requires the four certificates bit-exact with the pinned
- digests (`quantum-conformance/fabric/expected.json`), every vector's statevector hash,
- every reject file refused with its class, and a circuit wider than the member holds
- refused. The `checks` job builds the bundle, and a `browser` job runs the page on macOS.
### Packaging: refinement layers, stills, a piece's ladder, live windows and delivery
diff --git a/wai/tools/quantum-wgsl-check/Cargo.lock b/wai/tools/quantum-wgsl-check/Cargo.lock
index b602be6f5..9e381235d 100644
--- a/wai/tools/quantum-wgsl-check/Cargo.lock
+++ b/wai/tools/quantum-wgsl-check/Cargo.lock
@@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock
index 243c1e33d..f86bdffa4 100644
--- a/wai/wai-pack/Cargo.lock
+++ b/wai/wai-pack/Cargo.lock
@@ -698,7 +698,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock
index dbe6aa65b..8855b1a3a 100644
--- a/wai/wai-quantum-serve/Cargo.lock
+++ b/wai/wai-quantum-serve/Cargo.lock
@@ -589,7 +589,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock
index eb384ba0d..0f6af4536 100644
--- a/wai/wai-quantum/Cargo.lock
+++ b/wai/wai-quantum/Cargo.lock
@@ -995,7 +995,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml
index 2677358f7..24aaaddf9 100644
--- a/wai/wai-quantum/Cargo.toml
+++ b/wai/wai-quantum/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock
index c32febd24..369bd6d37 100644
--- a/wai/wai-rs/Cargo.lock
+++ b/wai/wai-rs/Cargo.lock
@@ -4459,7 +4459,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock
index 9b9edf0e5..39cd17aa5 100644
--- a/wai/wai-web/Cargo.lock
+++ b/wai/wai-web/Cargo.lock
@@ -1226,7 +1226,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.4.1"
+version = "0.4.2"
dependencies = [
"blake3",
"ed25519-dalek",
--
2.54.0 (Apple Git-157)