Quazim0t0 commited on
Commit
cad0868
·
verified ·
1 Parent(s): 93d9b2e

Web demo: metamorphic property tests (no-oracle correctness checks)

Browse files
Files changed (1) hide show
  1. web/test_metamorphic.js +136 -0
web/test_metamorphic.js ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Metamorphic tests: correctness properties that need NO reference implementation.
2
+ //
3
+ // Differential testing (kernel vs mirror) is only as strong as the independence
4
+ // of the two implementations, and every time you tune the oracle to agree with
5
+ // the thing it's checking, you spend some of that independence. It is also
6
+ // structurally blind to a bug in SHARED source: both replicas compile the same
7
+ // WGSL string, so a mutation there propagates to every device and every mirror
8
+ // tuned to match it.
9
+ //
10
+ // These properties come from the DEFINITION of a block-scaled batched GEMM, not
11
+ // from any implementation of one. When one fails, there is no referee question:
12
+ // the behaviour is wrong, regardless of which side is "right". They hold exactly
13
+ // (not approximately) because per-row/per-column quantization commutes with
14
+ // permutation, and because int32 accumulation is exactly associative.
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const V = require("./public/verified_core.js");
18
+ const L = { mul: new Int16Array(fs.readFileSync(path.join(__dirname, "public", "mul_lut.bin")).buffer.slice(0)) };
19
+
20
+ const randf = (n, f) => Float32Array.from({ length: n }, f || (() => Math.random() * 2 - 1));
21
+
22
+ // The kernel under test: float in, float out, block-scaled through the units.
23
+ // Swap in a mutant to prove the properties actually bite.
24
+ function makeKernel(bug) {
25
+ return async (Xf, Wf, d) => {
26
+ const { m, k, n } = d, batch = d.batch || 1;
27
+ const x = V.quantizeRows(Xf, batch * m, k);
28
+ let wq, ws;
29
+ if (batch === 1) { const w = V.quantizeCols(Wf, k, n); wq = w.q; ws = w.s; }
30
+ else {
31
+ wq = new Int8Array(batch * k * n); ws = new Float32Array(batch * n);
32
+ for (let bz = 0; bz < batch; bz++) {
33
+ const w = V.quantizeCols(Wf.subarray(bz * k * n, (bz + 1) * k * n), k, n);
34
+ wq.set(w.q, bz * k * n); ws.set(w.s, bz * n);
35
+ }
36
+ }
37
+ if (bug === "batchStride") { // ignores the batch offset on W
38
+ const d2 = { ...d };
39
+ const out = new Float32Array(batch * m * n);
40
+ for (let bz = 0; bz < batch; bz++) {
41
+ const o = V.bgemmJS(x.q.subarray(bz * m * k, (bz + 1) * m * k), wq.subarray(0, k * n),
42
+ x.s.subarray(bz * m, (bz + 1) * m), ws.subarray(0, n),
43
+ { ...d2, batch: 1 }, L);
44
+ out.set(o, bz * m * n);
45
+ }
46
+ return out;
47
+ }
48
+ if (bug === "rowSwap") { // transposes two output rows
49
+ const out = V.bgemmJS(x.q, wq, x.s, ws, d, L);
50
+ if (m > 1) for (let j = 0; j < n; j++) { const t = out[0 * n + j]; out[0 * n + j] = out[1 * n + j]; out[1 * n + j] = t; }
51
+ return out;
52
+ }
53
+ return V.bgemmJS(x.q, wq, x.s, ws, d, L);
54
+ };
55
+ }
56
+
57
+ const eq = (a, b) => { for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; };
58
+
59
+ // ---- the properties ---------------------------------------------------------
60
+ const PROPS = {
61
+ // a zero row of A must produce a zero row of output, whatever the scales are
62
+ async zeroRow(K) {
63
+ const d = { m: 6, k: 32, n: 5, batch: 1 };
64
+ const A = randf(d.m * d.k), B = randf(d.k * d.n);
65
+ for (let p = 0; p < d.k; p++) A[2 * d.k + p] = 0;
66
+ const out = await K(A, B, d);
67
+ for (let j = 0; j < d.n; j++) if (out[2 * d.n + j] !== 0) return `row 2 was zeroed but output[2,${j}] = ${out[2 * d.n + j]}`;
68
+ return null;
69
+ },
70
+ // permuting rows of A must permute rows of the output the same way
71
+ async rowPermutation(K) {
72
+ const d = { m: 6, k: 32, n: 5, batch: 1 };
73
+ const A = randf(d.m * d.k), B = randf(d.k * d.n);
74
+ const perm = [3, 1, 5, 0, 4, 2];
75
+ const Ap = new Float32Array(A.length);
76
+ perm.forEach((src, dst) => Ap.set(A.subarray(src * d.k, (src + 1) * d.k), dst * d.k));
77
+ const out = await K(A, B, d), outP = await K(Ap, B, d);
78
+ for (let r = 0; r < d.m; r++)
79
+ for (let j = 0; j < d.n; j++)
80
+ if (outP[r * d.n + j] !== out[perm[r] * d.n + j])
81
+ return `permuting rows of A did not permute the output at [${r},${j}]`;
82
+ return null;
83
+ },
84
+ // permuting columns of B must permute columns of the output the same way
85
+ async colPermutation(K) {
86
+ const d = { m: 4, k: 32, n: 5, batch: 1 };
87
+ const A = randf(d.m * d.k), B = randf(d.k * d.n);
88
+ const perm = [2, 0, 4, 1, 3];
89
+ const Bp = new Float32Array(B.length);
90
+ for (let p = 0; p < d.k; p++) perm.forEach((src, dst) => { Bp[p * d.n + dst] = B[p * d.n + src]; });
91
+ const out = await K(A, B, d), outP = await K(A, Bp, d);
92
+ for (let r = 0; r < d.m; r++)
93
+ for (let j = 0; j < d.n; j++)
94
+ if (outP[r * d.n + j] !== out[r * d.n + perm[j]])
95
+ return `permuting columns of B did not permute the output at [${r},${j}]`;
96
+ return null;
97
+ },
98
+ // a batched call must equal running each batch element on its own
99
+ async batchDecomposition(K) {
100
+ const d = { m: 4, k: 32, n: 5, batch: 3 };
101
+ const A = randf(d.batch * d.m * d.k), B = randf(d.batch * d.k * d.n);
102
+ const together = await K(A, B, d);
103
+ for (let bz = 0; bz < d.batch; bz++) {
104
+ const alone = await K(A.subarray(bz * d.m * d.k, (bz + 1) * d.m * d.k),
105
+ B.subarray(bz * d.k * d.n, (bz + 1) * d.k * d.n), { ...d, batch: 1 });
106
+ const slice = together.subarray(bz * d.m * d.n, (bz + 1) * d.m * d.n);
107
+ if (!eq(alone, slice)) return `batch element ${bz} differs when computed alone vs batched`;
108
+ }
109
+ return null;
110
+ },
111
+ };
112
+
113
+ (async () => {
114
+ let pass = true;
115
+ const ok = (c, msg) => { console.log(`${c ? " ok " : " FAIL"} ${msg}`); if (!c) pass = false; };
116
+
117
+ console.log("\nproperties hold for the real kernel (no reference implementation used):");
118
+ for (const [name, prop] of Object.entries(PROPS)) {
119
+ const bad = await prop(makeKernel(null));
120
+ ok(bad === null, `${name}${bad ? " -> " + bad : ""}`);
121
+ }
122
+
123
+ // The point: these catch bugs with NO oracle. A mutation living in source
124
+ // shared by every replica would sail past the probe and past a mirror tuned
125
+ // to agree with it. It cannot sail past arithmetic that must be true.
126
+ console.log("\nthe same properties catch bugs with no oracle to compare against:");
127
+ const strideBad = await PROPS.batchDecomposition(makeKernel("batchStride"));
128
+ ok(strideBad !== null, `batchDecomposition catches a dropped batch stride (${strideBad || "MISSED"})`);
129
+ const swapBad = await PROPS.rowPermutation(makeKernel("rowSwap"));
130
+ ok(swapBad !== null, `rowPermutation catches swapped output rows (${swapBad || "MISSED"})`);
131
+ const swapZero = await PROPS.zeroRow(makeKernel("rowSwap"));
132
+ console.log(` note zeroRow vs the same rowSwap bug: ${swapZero ? "caught" : "missed (properties are partial, not a proof)"}`);
133
+
134
+ console.log(pass ? "\nMETAMORPHIC TEST PASSED" : "\nMETAMORPHIC TEST FAILED");
135
+ process.exit(pass ? 0 : 1);
136
+ })();