File size: 13,250 Bytes
cad0868
 
 
 
 
 
 
 
 
 
 
 
 
 
54e1054
 
 
 
 
 
 
 
 
 
 
cad0868
 
 
 
 
 
 
 
 
 
 
 
9ae1216
 
 
 
 
 
 
 
 
 
cad0868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54e1054
 
 
 
 
 
 
 
 
 
cad0868
 
 
 
 
 
 
 
9ae1216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54e1054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cad0868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ae1216
 
 
 
 
 
54e1054
 
 
 
 
 
cad0868
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// Metamorphic tests: correctness properties that need NO reference implementation.
//
// Differential testing (kernel vs mirror) is only as strong as the independence
// of the two implementations, and every time you tune the oracle to agree with
// the thing it's checking, you spend some of that independence. It is also
// structurally blind to a bug in SHARED source: both replicas compile the same
// WGSL string, so a mutation there propagates to every device and every mirror
// tuned to match it.
//
// These properties come from the DEFINITION of a block-scaled batched GEMM, not
// from any implementation of one. When one fails, there is no referee question:
// the behaviour is wrong, regardless of which side is "right". They hold exactly
// (not approximately) because per-row/per-column quantization commutes with
// permutation, and because int32 accumulation is exactly associative.
//
// The suite is TWO species of check, and the distinction matters:
//   RELATIONS (permutation, zero-row, batch, sensitivity) — compare calls to
//     each other. Provably blind to value bugs: if out satisfies every
//     relation, so does c·out. An external bug corpus scored exactly this
//     hole (2/2 on loop bugs, 0/2 on math bugs).
//   DEFINITIONAL ABSOLUTES (reluRange, unitScaleAnchor) — points where the
//     spec pins the value itself: ReLU output cannot be negative, and at unit
//     scales the output IS the integer dot product. Still no reference
//     implementation anywhere — the expected values are plain integer
//     arithmetic — but they close the value-bug hole the relations cannot.
const fs = require("fs");
const path = require("path");
const V = require("./public/verified_core.js");
const L = { mul: new Int16Array(fs.readFileSync(path.join(__dirname, "public", "mul_lut.bin")).buffer.slice(0)) };

const randf = (n, f) => Float32Array.from({ length: n }, f || (() => Math.random() * 2 - 1));

// The kernel under test: float in, float out, block-scaled through the units.
// Swap in a mutant to prove the properties actually bite.
function makeKernel(bug) {
  return async (Xf, Wf, d) => {
    const { m, k, n } = d, batch = d.batch || 1;
    if (bug === "accOverwrite") {                 // corpus: matmul_triton_buggy
      const q = V.quantizeRows(Xf, batch * m, k), w = V.quantizeCols(Wf, k, n);
      const out = d.acc ? new Int32Array(batch * m * n) : new Float32Array(batch * m * n);
      for (let i = 0; i < m; i++) for (let j = 0; j < n; j++) {
        let a = 0;
        for (let p = 0; p < k; p++) a = L.mul[(q.q[i * k + p] & 0xFF) * 256 + (w.q[p * n + j] & 0xFF)];
        out[i * n + j] = d.acc ? a : V.epi(a, q.s[i], w.s[j]);
      }
      return out;
    }
    const x = V.quantizeRows(Xf, batch * m, k);
    let wq, ws;
    if (batch === 1) { const w = V.quantizeCols(Wf, k, n); wq = w.q; ws = w.s; }
    else {
      wq = new Int8Array(batch * k * n); ws = new Float32Array(batch * n);
      for (let bz = 0; bz < batch; bz++) {
        const w = V.quantizeCols(Wf.subarray(bz * k * n, (bz + 1) * k * n), k, n);
        wq.set(w.q, bz * k * n); ws.set(w.s, bz * n);
      }
    }
    if (bug === "batchStride") {          // ignores the batch offset on W
      const d2 = { ...d };
      const out = new Float32Array(batch * m * n);
      for (let bz = 0; bz < batch; bz++) {
        const o = V.bgemmJS(x.q.subarray(bz * m * k, (bz + 1) * m * k), wq.subarray(0, k * n),
                            x.s.subarray(bz * m, (bz + 1) * m), ws.subarray(0, n),
                            { ...d2, batch: 1 }, L);
        out.set(o, bz * m * n);
      }
      return out;
    }
    if (bug === "rowSwap") {              // transposes two output rows
      const out = V.bgemmJS(x.q, wq, x.s, ws, d, L);
      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; }
      return out;
    }
    if (bug === "factor2") {              // corpus: gelu_triton_buggy — uniform 2x
      const out = V.bgemmJS(x.q, wq, x.s, ws, d, L);
      for (let i = 0; i < out.length; i++) out[i] = Math.fround(out[i] * 2);
      return out;
    }
    if (bug === "leakyAlpha") {           // corpus: leaky_relu_buggy — leaks instead of clamping
      const out = V.bgemmJS(x.q, wq, x.s, ws, { ...d, relu: false }, L);
      if (d.relu) for (let i = 0; i < out.length; i++) if (out[i] < 0) out[i] = Math.fround(out[i] * 0.1);
      return out;
    }
    return V.bgemmJS(x.q, wq, x.s, ws, d, L);
  };
}

const eq = (a, b) => { for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; };

// ---- the properties ---------------------------------------------------------
const PROPS = {
  // NON-TRIVIALITY. Added after an external bug corpus scored the suite below
  // 0/4: the zero function satisfies every relation here, because zero is
  // permutation-equivariant, zero-row-preserving and batch-decomposable. Without
  // this, a kernel can pass the whole suite by doing nothing at all.
  async nonTriviality(K) {
    const d = { m: 6, k: 32, n: 5, batch: 1 };
    const out = await K(randf(d.m * d.k), randf(d.k * d.n), d);
    let nz = 0;
    for (let i = 0; i < out.length; i++) if (out[i] !== 0) nz++;
    if (nz < out.length / 4) return `output is ${out.length - nz}/${out.length} zeros`;
    return null;
  },
  // SENSITIVITY. Every element of A must be able to move its output row. Catches
  // an accumulator that overwrites instead of accumulating (acc= for acc+=):
  // every structural relation still holds, but only the last k contributes.
  // Measured on the raw accumulator, perturbed by a sign flip that leaves the
  // row absmax alone — otherwise the perturbation moves the output through the
  // quantization SCALE and the property proves nothing.
  async sensitivity(K) {
    const d = { m: 6, k: 32, n: 5, batch: 1, acc: true };
    const A = randf(d.m * d.k), B = randf(d.k * d.n);
    for (let p = 0; p < d.k; p++) A[1 * d.k + p] = 0.3;
    A[1 * d.k + 0] = 1.0;                               // pin the absmax at p=0
    const s0 = V.quantizeRows(A, d.m, d.k).s[1];
    const base = await K(A, B, d);
    for (let p = 1; p < d.k; p++) {
      const A2 = Float32Array.from(A);
      A2[1 * d.k + p] = -0.3;
      if (V.quantizeRows(A2, d.m, d.k).s[1] !== s0) continue;   // inconclusive
      const o2 = await K(A2, B, d);
      let moved = false;
      for (let j = 0; j < d.n; j++) if (o2[1 * d.n + j] !== base[1 * d.n + j]) { moved = true; break; }
      if (!moved) return `A[.,${p}] does not affect its output row`;
    }
    return null;
  },
  // relu(x) >= 0 is part of the DEFINITION when the fused ReLU is on — a range
  // constraint, not a relation. Catches a wrong negative slope, which every
  // relation survives (the structure of a leak is fine; its sign is not).
  async reluRange(K) {
    const d = { m: 6, k: 32, n: 5, batch: 1, relu: true };
    const out = await K(randf(d.m * d.k), randf(d.k * d.n), d);
    for (let i = 0; i < out.length; i++)
      if (out[i] < 0) return `negative output ${out[i]} at [${(i / d.n) | 0},${i % d.n}] under fused ReLU`;
    return null;
  },
  // With unit scales the dequant is the identity, so the definition pins
  // ABSOLUTE values: out must equal the exact integer dot product. No RELATION
  // can catch a uniform c× — if out satisfies every relation, c·out does too —
  // so the suite needs one point where the spec fixes the scale. Inputs are
  // floats that quantize exactly (row/col absmax = 127 ⇒ scale 1), and the
  // expected values are plain integer arithmetic: no reference implementation.
  async unitScaleAnchor(K) {
    const d = { m: 2, k: 4, n: 2, batch: 1 };
    const A = Float32Array.from([127, 1, -2, 3,
                                 0, 5, -127, 2]);
    const B = Float32Array.from([127, 3,        // k×n, each COLUMN has absmax 127
                                 -1, 127,
                                 2, -5,
                                 0, 1]);
    const out = await K(A, B, d);
    for (let i = 0; i < d.m; i++)
      for (let j = 0; j < d.n; j++) {
        let dot = 0;
        for (let p = 0; p < d.k; p++) dot += A[i * d.k + p] * B[p * d.n + j];
        if (out[i * d.n + j] !== dot) return `unit-scale output [${i},${j}] = ${out[i * d.n + j]}, definition says ${dot}`;
      }
    return null;
  },
  // a zero row of A must produce a zero row of output, whatever the scales are
  async zeroRow(K) {
    const d = { m: 6, k: 32, n: 5, batch: 1 };
    const A = randf(d.m * d.k), B = randf(d.k * d.n);
    for (let p = 0; p < d.k; p++) A[2 * d.k + p] = 0;
    const out = await K(A, B, d);
    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]}`;
    return null;
  },
  // permuting rows of A must permute rows of the output the same way
  async rowPermutation(K) {
    const d = { m: 6, k: 32, n: 5, batch: 1 };
    const A = randf(d.m * d.k), B = randf(d.k * d.n);
    const perm = [3, 1, 5, 0, 4, 2];
    const Ap = new Float32Array(A.length);
    perm.forEach((src, dst) => Ap.set(A.subarray(src * d.k, (src + 1) * d.k), dst * d.k));
    const out = await K(A, B, d), outP = await K(Ap, B, d);
    for (let r = 0; r < d.m; r++)
      for (let j = 0; j < d.n; j++)
        if (outP[r * d.n + j] !== out[perm[r] * d.n + j])
          return `permuting rows of A did not permute the output at [${r},${j}]`;
    return null;
  },
  // permuting columns of B must permute columns of the output the same way
  async colPermutation(K) {
    const d = { m: 4, k: 32, n: 5, batch: 1 };
    const A = randf(d.m * d.k), B = randf(d.k * d.n);
    const perm = [2, 0, 4, 1, 3];
    const Bp = new Float32Array(B.length);
    for (let p = 0; p < d.k; p++) perm.forEach((src, dst) => { Bp[p * d.n + dst] = B[p * d.n + src]; });
    const out = await K(A, B, d), outP = await K(A, Bp, d);
    for (let r = 0; r < d.m; r++)
      for (let j = 0; j < d.n; j++)
        if (outP[r * d.n + j] !== out[r * d.n + perm[j]])
          return `permuting columns of B did not permute the output at [${r},${j}]`;
    return null;
  },
  // a batched call must equal running each batch element on its own
  async batchDecomposition(K) {
    const d = { m: 4, k: 32, n: 5, batch: 3 };
    const A = randf(d.batch * d.m * d.k), B = randf(d.batch * d.k * d.n);
    const together = await K(A, B, d);
    for (let bz = 0; bz < d.batch; bz++) {
      const alone = await K(A.subarray(bz * d.m * d.k, (bz + 1) * d.m * d.k),
                           B.subarray(bz * d.k * d.n, (bz + 1) * d.k * d.n), { ...d, batch: 1 });
      const slice = together.subarray(bz * d.m * d.n, (bz + 1) * d.m * d.n);
      if (!eq(alone, slice)) return `batch element ${bz} differs when computed alone vs batched`;
    }
    return null;
  },
};

(async () => {
  let pass = true;
  const ok = (c, msg) => { console.log(`${c ? "  ok  " : "  FAIL"}  ${msg}`); if (!c) pass = false; };

  console.log("\nproperties hold for the real kernel (no reference implementation used):");
  for (const [name, prop] of Object.entries(PROPS)) {
    const bad = await prop(makeKernel(null));
    ok(bad === null, `${name}${bad ? "  -> " + bad : ""}`);
  }

  // The point: these catch bugs with NO oracle. A mutation living in source
  // shared by every replica would sail past the probe and past a mirror tuned
  // to agree with it. It cannot sail past arithmetic that must be true.
  console.log("\nthe same properties catch bugs with no oracle to compare against:");
  const strideBad = await PROPS.batchDecomposition(makeKernel("batchStride"));
  ok(strideBad !== null, `batchDecomposition catches a dropped batch stride  (${strideBad || "MISSED"})`);
  const swapBad = await PROPS.rowPermutation(makeKernel("rowSwap"));
  ok(swapBad !== null, `rowPermutation catches swapped output rows  (${swapBad || "MISSED"})`);
  const swapZero = await PROPS.zeroRow(makeKernel("rowSwap"));
  console.log(`  note   zeroRow vs the same rowSwap bug: ${swapZero ? "caught" : "missed (properties are partial, not a proof)"}`);
  // a bug from someone else's taxonomy, not mine: acc= instead of acc+=. Every
  // structural relation survives it, which is why sensitivity had to exist.
  const accSens = await PROPS.sensitivity(makeKernel("accOverwrite"));
  const accPerm = await PROPS.rowPermutation(makeKernel("accOverwrite"));
  ok(accSens !== null, `sensitivity catches acc= instead of acc+=  (${accSens || "MISSED"})`);
  console.log(`  note   rowPermutation vs that same acc= bug: ${accPerm ? "caught" : "missed — it is structure-preserving, which is the whole trap"}`);
  // value bugs: invisible to every RELATION (c·out satisfies whatever out
  // does), caught by the definitional absolutes — range and unit-scale anchor
  const leakBad = await PROPS.reluRange(makeKernel("leakyAlpha"));
  ok(leakBad !== null, `reluRange catches a wrong leaky slope  (${leakBad || "MISSED"})`);
  const facBad = await PROPS.unitScaleAnchor(makeKernel("factor2"));
  ok(facBad !== null, `unitScaleAnchor catches a uniform 2x  (${facBad || "MISSED"})`);

  console.log(pass ? "\nMETAMORPHIC TEST PASSED" : "\nMETAMORPHIC TEST FAILED");
  process.exit(pass ? 0 : 1);
})();