Web demo: block-scaled INT8 + batched/fused GEMM kernels
Browse files- web/public/app.js +2 -2
- web/public/transformer.js +48 -25
- web/public/verified_core.js +83 -1
- web/public/webgpu.js +137 -2
web/public/app.js
CHANGED
|
@@ -806,7 +806,7 @@ function cfgSliders() { return [ui.cfgC, ui.cfgT, ui.cfgB, ui.cfgSteps, ui.cfgLr
|
|
| 806 |
// (re)build the mini transformer — deterministic shared init (same seeds on
|
| 807 |
// every peer); each device samples its own batch windows from the shared corpus
|
| 808 |
function buildModel(cfg) {
|
| 809 |
-
model = Transformer.init(cfg, L, compute
|
| 810 |
trainedSteps = 0;
|
| 811 |
}
|
| 812 |
|
|
@@ -856,7 +856,7 @@ function buildModel(cfg) {
|
|
| 856 |
ui.start.disabled = true;
|
| 857 |
return; // no signaling, no training
|
| 858 |
}
|
| 859 |
-
ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} ·
|
| 860 |
// tokenizer must load BEFORE the model is built (it sets the vocab size)
|
| 861 |
try {
|
| 862 |
const name = await Transformer.loadTokenizer();
|
|
|
|
| 806 |
// (re)build the mini transformer — deterministic shared init (same seeds on
|
| 807 |
// every peer); each device samples its own batch windows from the shared corpus
|
| 808 |
function buildModel(cfg) {
|
| 809 |
+
model = Transformer.init(cfg, L, compute); // compute.bgemm = fused batched GPU path
|
| 810 |
trainedSteps = 0;
|
| 811 |
}
|
| 812 |
|
|
|
|
| 856 |
ui.start.disabled = true;
|
| 857 |
return; // no signaling, no training
|
| 858 |
}
|
| 859 |
+
ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label} · block-scaled INT8, batched + fused epilogue, through verified units`;
|
| 860 |
// tokenizer must load BEFORE the model is built (it sets the vocab size)
|
| 861 |
try {
|
| 862 |
const name = await Transformer.loadTokenizer();
|
web/public/transformer.js
CHANGED
|
@@ -111,17 +111,17 @@
|
|
| 111 |
const streamFineWebEdu = () => streamDataset(DEFAULT_DS);
|
| 112 |
function datasetName() { return DATASET; }
|
| 113 |
|
| 114 |
-
// ---- verified matmul:
|
| 115 |
-
//
|
| 116 |
-
//
|
| 117 |
-
//
|
| 118 |
-
//
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
return V.lutMatmul3(Xf, Wf, m, k, n, ctx.L, ctx.matmulInt8);
|
| 122 |
}
|
| 123 |
-
|
| 124 |
-
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
// ---- layernorm (no affine) -------------------------------------------------
|
|
@@ -148,7 +148,9 @@
|
|
| 148 |
|
| 149 |
// ---- model -----------------------------------------------------------------
|
| 150 |
// cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
|
| 153 |
let seed = 100;
|
| 154 |
const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
|
|
@@ -156,7 +158,7 @@
|
|
| 156 |
const add = (name, w) => { params.push(w); names.push(name); return w; };
|
| 157 |
const m = {
|
| 158 |
cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
|
| 159 |
-
ctx: { L,
|
| 160 |
emb: add("emb", mk(vocabSize() * c, 0.08)),
|
| 161 |
pos: add("pos", mk(cfg.t * c, 0.02)),
|
| 162 |
blocks: [], params, names,
|
|
@@ -217,22 +219,43 @@
|
|
| 217 |
const ctxOut = new Float32Array(BT * C);
|
| 218 |
cb.att = []; // per (b,h): softmax probs
|
| 219 |
const scale = 1 / Math.sqrt(hd);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 221 |
-
const
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
for (let ti = 0; ti < T; ti++) {
|
| 227 |
let mx = -1e30;
|
| 228 |
-
for (let tj = 0; tj <= ti; tj++) mx = Math.max(mx,
|
| 229 |
let z = 0;
|
| 230 |
-
for (let tj = 0; tj <= ti; tj++) { const e = Math.exp(
|
| 231 |
-
for (let tj = 0; tj <= ti; tj++)
|
| 232 |
}
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
cb.ctxOut = ctxOut;
|
| 238 |
const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
|
|
@@ -240,9 +263,9 @@
|
|
| 240 |
for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attnOut[i];
|
| 241 |
cb.x2 = x2;
|
| 242 |
const l2 = lnFwd(x2, BT, C); cb.ln2 = l2;
|
| 243 |
-
const h1 = await vmm(l2.y, bl.W1, BT, C, hidden, ctx);
|
| 244 |
const mask = new Uint8Array(h1.length);
|
| 245 |
-
for (let i = 0; i < h1.length; i++)
|
| 246 |
cb.h1 = h1; cb.mask = mask;
|
| 247 |
const mlpOut = await vmm(h1, bl.W2, BT, hidden, C, ctx);
|
| 248 |
x = new Float32Array(BT * C);
|
|
|
|
| 111 |
const streamFineWebEdu = () => streamDataset(DEFAULT_DS);
|
| 112 |
function datasetName() { return DATASET; }
|
| 113 |
|
| 114 |
+
// ---- verified matmul: block-scaled INT8 through the units ------------------
|
| 115 |
+
// CUTLASS ex. 67/81 blockwise scaling: per-row activation scales × per-column
|
| 116 |
+
// weight scales, one exact LUT/DP4A GEMM, dequant (+ optional fused ReLU) in
|
| 117 |
+
// the kernel epilogue (ex. 12). Replaces the per-tensor 3-pass: same outlier
|
| 118 |
+
// robustness at one third of the unit ops.
|
| 119 |
+
async function vmm(Xf, Wf, m, k, n, ctx, relu) {
|
| 120 |
+
return V.vgemmBlock(Xf, Wf, { m, k, n, batch: 1, relu: !!relu }, ctx.L, ctx.bgemm);
|
|
|
|
| 121 |
}
|
| 122 |
+
// batched: one dispatch for all B×H attention-head problems (ex. 05/24)
|
| 123 |
+
async function vmmBatched(Xf, Wf, m, k, n, batch, ctx) {
|
| 124 |
+
return V.vgemmBlock(Xf, Wf, { m, k, n, batch }, ctx.L, ctx.bgemm);
|
| 125 |
}
|
| 126 |
|
| 127 |
// ---- layernorm (no affine) -------------------------------------------------
|
|
|
|
| 148 |
|
| 149 |
// ---- model -----------------------------------------------------------------
|
| 150 |
// cfg: { c: width, t: seq len, b: batch/device, layers, heads, steps, lr }
|
| 151 |
+
// engine: the Compute backend object ({bgemm} for the fused WebGPU path) or a
|
| 152 |
+
// legacy matmulInt8 function (Node tests, inference kit) -> CPU LUT mirror
|
| 153 |
+
function init(cfg, L, engine) {
|
| 154 |
const c = cfg.c, layers = cfg.layers || 2, heads = cfg.heads || 2, hidden = 2 * c;
|
| 155 |
let seed = 100;
|
| 156 |
const mk = (nEl, scale) => { const w = randn(nEl, mulberry32(seed++)); for (let i = 0; i < nEl; i++) w[i] *= scale; return w; };
|
|
|
|
| 158 |
const add = (name, w) => { params.push(w); names.push(name); return w; };
|
| 159 |
const m = {
|
| 160 |
cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
|
| 161 |
+
ctx: { L, bgemm: (engine && engine.bgemm) || null },
|
| 162 |
emb: add("emb", mk(vocabSize() * c, 0.08)),
|
| 163 |
pos: add("pos", mk(cfg.t * c, 0.02)),
|
| 164 |
blocks: [], params, names,
|
|
|
|
| 219 |
const ctxOut = new Float32Array(BT * C);
|
| 220 |
cb.att = []; // per (b,h): softmax probs
|
| 221 |
const scale = 1 / Math.sqrt(hd);
|
| 222 |
+
// gather every head into batched contiguous layouts, then run ALL B×H
|
| 223 |
+
// score GEMMs in ONE batched dispatch (and all attn·V in another) —
|
| 224 |
+
// instead of 2·B·H tiny sequential matmuls
|
| 225 |
+
const BH = B * heads;
|
| 226 |
+
const qb = new Float32Array(BH * T * hd), kb = new Float32Array(BH * T * hd);
|
| 227 |
+
const vb = new Float32Array(BH * T * hd), ktb = new Float32Array(BH * hd * T);
|
| 228 |
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 229 |
+
const bz = bi * heads + h;
|
| 230 |
+
for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++) {
|
| 231 |
+
const src = (bi * T + ti) * C + h * hd + j;
|
| 232 |
+
qb[(bz * T + ti) * hd + j] = q[src];
|
| 233 |
+
kb[(bz * T + ti) * hd + j] = k[src];
|
| 234 |
+
vb[(bz * T + ti) * hd + j] = v[src];
|
| 235 |
+
ktb[(bz * hd + j) * T + ti] = k[src]; // kᵀ per head, for scores
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
const sAll = await vmmBatched(qb, ktb, T, hd, T, BH, ctx); // scores through the units
|
| 239 |
+
const aAll = new Float32Array(BH * T * T); // causal softmax
|
| 240 |
+
for (let bz = 0; bz < BH; bz++) {
|
| 241 |
+
const so = bz * T * T;
|
| 242 |
for (let ti = 0; ti < T; ti++) {
|
| 243 |
let mx = -1e30;
|
| 244 |
+
for (let tj = 0; tj <= ti; tj++) mx = Math.max(mx, sAll[so + ti * T + tj] * scale);
|
| 245 |
let z = 0;
|
| 246 |
+
for (let tj = 0; tj <= ti; tj++) { const e = Math.exp(sAll[so + ti * T + tj] * scale - mx); aAll[so + ti * T + tj] = e; z += e; }
|
| 247 |
+
for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
|
| 248 |
}
|
| 249 |
+
}
|
| 250 |
+
const cAll = await vmmBatched(aAll, vb, T, T, hd, BH, ctx); // attn·V through the units
|
| 251 |
+
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 252 |
+
const bz = bi * heads + h;
|
| 253 |
+
for (let ti = 0; ti < T; ti++) for (let j = 0; j < hd; j++)
|
| 254 |
+
ctxOut[(bi * T + ti) * C + h * hd + j] = cAll[(bz * T + ti) * hd + j];
|
| 255 |
+
cb.att.push({ a: aAll.subarray(bz * T * T, (bz + 1) * T * T),
|
| 256 |
+
qh: qb.subarray(bz * T * hd, (bz + 1) * T * hd),
|
| 257 |
+
kh: kb.subarray(bz * T * hd, (bz + 1) * T * hd),
|
| 258 |
+
vh: vb.subarray(bz * T * hd, (bz + 1) * T * hd) });
|
| 259 |
}
|
| 260 |
cb.ctxOut = ctxOut;
|
| 261 |
const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
|
|
|
|
| 263 |
for (let i = 0; i < x2.length; i++) x2[i] = x[i] + attnOut[i];
|
| 264 |
cb.x2 = x2;
|
| 265 |
const l2 = lnFwd(x2, BT, C); cb.ln2 = l2;
|
| 266 |
+
const h1 = await vmm(l2.y, bl.W1, BT, C, hidden, ctx, true); // ReLU fused in the epilogue
|
| 267 |
const mask = new Uint8Array(h1.length);
|
| 268 |
+
for (let i = 0; i < h1.length; i++) if (h1[i] > 0) mask[i] = 1;
|
| 269 |
cb.h1 = h1; cb.mask = mask;
|
| 270 |
const mlpOut = await vmm(h1, bl.W2, BT, hidden, C, ctx);
|
| 271 |
x = new Float32Array(BT * C);
|
web/public/verified_core.js
CHANGED
|
@@ -66,6 +66,87 @@
|
|
| 66 |
return combine3(hh, hl, lh, x, w, m * n);
|
| 67 |
}
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
// one verified layer forward; returns float out (+ cache for STE backward).
|
| 70 |
// Every product goes through the verified INT8 multiply (mul8 LUT) with exact
|
| 71 |
// int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
|
|
@@ -116,7 +197,8 @@
|
|
| 116 |
for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
|
| 117 |
}
|
| 118 |
|
| 119 |
-
const api = { quantize, quantize2,
|
|
|
|
| 120 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
|
| 121 |
else { TC = root.TrainCore; root.Verified = api; }
|
| 122 |
})(typeof self !== "undefined" ? self : this);
|
|
|
|
| 66 |
return combine3(hh, hl, lh, x, w, m * n);
|
| 67 |
}
|
| 68 |
|
| 69 |
+
// ---- block-scaled verified GEMM (CUTLASS ex. 67/81 blockwise scaling) ------
|
| 70 |
+
// Per-ROW scales for the activations and per-COLUMN scales for the weights:
|
| 71 |
+
// the integer math through the mul8 LUT is completely unchanged — only the
|
| 72 |
+
// dequant uses rs[row]·cs[col] instead of one tensor-wide product, so a single
|
| 73 |
+
// outlier no longer crushes the quantization resolution of every other
|
| 74 |
+
// row/column. One LUT pass at this granularity beats the per-tensor 3-pass.
|
| 75 |
+
function quantizeRows(X, rows, cols) {
|
| 76 |
+
const q = new Int8Array(rows * cols), s = new Float32Array(rows);
|
| 77 |
+
for (let r = 0; r < rows; r++) {
|
| 78 |
+
let mx = 0;
|
| 79 |
+
for (let c = 0; c < cols; c++) { const a = Math.abs(X[r * cols + c]); if (a > mx) mx = a; }
|
| 80 |
+
const sc = Math.max(mx / 127, 1e-8); s[r] = sc;
|
| 81 |
+
for (let c = 0; c < cols; c++) {
|
| 82 |
+
const v = Math.round(X[r * cols + c] / sc);
|
| 83 |
+
q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
return { q, s };
|
| 87 |
+
}
|
| 88 |
+
function quantizeCols(W, rows, cols) {
|
| 89 |
+
const q = new Int8Array(rows * cols), s = new Float32Array(cols);
|
| 90 |
+
for (let c = 0; c < cols; c++) {
|
| 91 |
+
let mx = 0;
|
| 92 |
+
for (let r = 0; r < rows; r++) { const a = Math.abs(W[r * cols + c]); if (a > mx) mx = a; }
|
| 93 |
+
s[c] = Math.max(mx / 127, 1e-8);
|
| 94 |
+
}
|
| 95 |
+
for (let r = 0; r < rows; r++)
|
| 96 |
+
for (let c = 0; c < cols; c++) {
|
| 97 |
+
const v = Math.round(W[r * cols + c] / s[c]);
|
| 98 |
+
q[r * cols + c] = v < -128 ? -128 : v > 127 ? 127 : v;
|
| 99 |
+
}
|
| 100 |
+
return { q, s };
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// CPU mirror of the fused GPU kernel: batched int8 GEMM through the LUT with
|
| 104 |
+
// the epilogue (block dequant + optional ReLU) applied before returning —
|
| 105 |
+
// exactly what the WGSL kernel does on-device.
|
| 106 |
+
function bgemmJS(Xq, Wq, rs, cs, d, L) {
|
| 107 |
+
const { m, k, n } = d, batch = d.batch || 1, relu = !!d.relu, mul = L.mul;
|
| 108 |
+
const out = new Float32Array(batch * m * n);
|
| 109 |
+
const acc = new Int32Array(n);
|
| 110 |
+
for (let bz = 0; bz < batch; bz++) {
|
| 111 |
+
const xo = bz * m * k, wo = bz * k * n, oo = bz * m * n, co = bz * n;
|
| 112 |
+
for (let i = 0; i < m; i++) {
|
| 113 |
+
acc.fill(0);
|
| 114 |
+
const xrow = xo + i * k;
|
| 115 |
+
for (let p = 0; p < k; p++) {
|
| 116 |
+
const au = (Xq[xrow + p] & 0xFF) * 256, wrow = wo + p * n;
|
| 117 |
+
for (let j = 0; j < n; j++) acc[j] += mul[au + (Wq[wrow + j] & 0xFF)];
|
| 118 |
+
}
|
| 119 |
+
const rscale = rs[bz * m + i], orow = oo + i * n;
|
| 120 |
+
for (let j = 0; j < n; j++) {
|
| 121 |
+
const v = acc[j] * rscale * cs[co + j];
|
| 122 |
+
out[orow + j] = relu && v < 0 ? 0 : v;
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
return out;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// block-scaled verified GEMM, float in → float out.
|
| 130 |
+
// d = { m, k, n, batch=1, relu=false }; X is (batch·m)×k, W is batch×(k×n)
|
| 131 |
+
// gpuBgemm (from webgpu.js) runs the batched kernel with the fused epilogue;
|
| 132 |
+
// without it the CPU LUT mirror runs. Every product goes through the units.
|
| 133 |
+
async function vgemmBlock(Xf, Wf, d, L, gpuBgemm) {
|
| 134 |
+
const { m, k, n } = d, batch = d.batch || 1;
|
| 135 |
+
const x = quantizeRows(Xf, batch * m, k);
|
| 136 |
+
let wq, ws;
|
| 137 |
+
if (batch === 1) {
|
| 138 |
+
const w = quantizeCols(Wf, k, n); wq = w.q; ws = w.s;
|
| 139 |
+
} else {
|
| 140 |
+
wq = new Int8Array(batch * k * n); ws = new Float32Array(batch * n);
|
| 141 |
+
for (let bz = 0; bz < batch; bz++) {
|
| 142 |
+
const w = quantizeCols(Wf.subarray(bz * k * n, (bz + 1) * k * n), k, n);
|
| 143 |
+
wq.set(w.q, bz * k * n); ws.set(w.s, bz * n);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
if (gpuBgemm) return gpuBgemm(x.q, wq, x.s, ws, d);
|
| 147 |
+
return bgemmJS(x.q, wq, x.s, ws, d, L);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
// one verified layer forward; returns float out (+ cache for STE backward).
|
| 151 |
// Every product goes through the verified INT8 multiply (mul8 LUT) with exact
|
| 152 |
// int32 accumulation — i.e. an emulated INT8 tensor-core GEMM — then dequant.
|
|
|
|
| 197 |
for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
|
| 198 |
}
|
| 199 |
|
| 200 |
+
const api = { quantize, quantize2, quantizeRows, quantizeCols, lutMatmulJS, lutMatmul3JS, lutMatmul3,
|
| 201 |
+
bgemmJS, vgemmBlock, linearFwd, forward, backward, splitApply };
|
| 202 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
|
| 203 |
else { TC = root.TrainCore; root.Verified = api; }
|
| 204 |
})(typeof self !== "undefined" ? self : this);
|
web/public/webgpu.js
CHANGED
|
@@ -54,6 +54,60 @@
|
|
| 54 |
C[row * n + col] = s;
|
| 55 |
}`;
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
async function loadLUTs(base) {
|
| 58 |
base = base || "";
|
| 59 |
const [mulB, reqB, reluB, meta] = await Promise.all([
|
|
@@ -98,8 +152,11 @@
|
|
| 98 |
const lut32 = new Int32Array(L.mul); // widen int16 -> int32
|
| 99 |
const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
| 100 |
device.queue.writeBuffer(lutBuf, 0, lut32);
|
|
|
|
|
|
|
| 101 |
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
|
| 102 |
-
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n)
|
|
|
|
| 103 |
|
| 104 |
// DP4A pipeline — only if the WGSL feature exists AND it reproduces the
|
| 105 |
// verified units exactly on random self-tests
|
|
@@ -118,7 +175,29 @@
|
|
| 118 |
for (let i = 0; i < ref.length; i++)
|
| 119 |
if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
|
| 120 |
}
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
} catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
|
| 123 |
}
|
| 124 |
|
|
@@ -168,6 +247,62 @@
|
|
| 168 |
return r.out;
|
| 169 |
}
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
|
| 172 |
function up(device, arr, usage) {
|
| 173 |
const b = mk(device, Math.max(16, arr.byteLength), usage);
|
|
|
|
| 54 |
C[row * n + col] = s;
|
| 55 |
}`;
|
| 56 |
|
| 57 |
+
// Batched block-scaled GEMM with a FUSED EPILOGUE (CUTLASS ex. 05/24 + 12):
|
| 58 |
+
// grid z = batch index, so ALL attention heads run in ONE dispatch, and the
|
| 59 |
+
// epilogue (block dequant rs·cs + optional ReLU) happens before the data
|
| 60 |
+
// leaves the GPU — f32 out, no int32 readback, no second pass in JS.
|
| 61 |
+
const WGSL_BG_LUT = `
|
| 62 |
+
@group(0) @binding(0) var<storage, read> Xq : array<i32>; // int8 byte per elem
|
| 63 |
+
@group(0) @binding(1) var<storage, read> Wq : array<i32>;
|
| 64 |
+
@group(0) @binding(2) var<storage, read> lut : array<i32>;
|
| 65 |
+
@group(0) @binding(3) var<storage, read> rs : array<f32>; // per (batch,row)
|
| 66 |
+
@group(0) @binding(4) var<storage, read> cs : array<f32>; // per (batch,col)
|
| 67 |
+
@group(0) @binding(5) var<storage, read_write> O : array<f32>;
|
| 68 |
+
@group(0) @binding(6) var<uniform> dims : vec4<u32>; // m, k, n, flags(1=relu)
|
| 69 |
+
@compute @workgroup_size(8, 8, 1)
|
| 70 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 71 |
+
let m = dims.x; let k = dims.y; let n = dims.z;
|
| 72 |
+
let row = gid.x; let col = gid.y; let bz = gid.z;
|
| 73 |
+
if (row >= m || col >= n) { return; }
|
| 74 |
+
var s : i32 = 0;
|
| 75 |
+
let xo = (bz * m + row) * k;
|
| 76 |
+
let wo = bz * k * n + col;
|
| 77 |
+
for (var p = 0u; p < k; p = p + 1u) {
|
| 78 |
+
let au = u32(Xq[xo + p] & 255);
|
| 79 |
+
let bu = u32(Wq[wo + p * n] & 255);
|
| 80 |
+
s = s + lut[au * 256u + bu];
|
| 81 |
+
}
|
| 82 |
+
var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
|
| 83 |
+
if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
|
| 84 |
+
O[(bz * m + row) * n + col] = v;
|
| 85 |
+
}`;
|
| 86 |
+
|
| 87 |
+
// same fused/batched kernel on the DP4A hardware path (Wᵀ packed per batch)
|
| 88 |
+
const WGSL_BG_DP4 = `
|
| 89 |
+
@group(0) @binding(0) var<storage, read> Xp : array<u32>;
|
| 90 |
+
@group(0) @binding(1) var<storage, read> Wp : array<u32>; // per-batch Wᵀ, packed
|
| 91 |
+
@group(0) @binding(2) var<storage, read> rs : array<f32>;
|
| 92 |
+
@group(0) @binding(3) var<storage, read> cs : array<f32>;
|
| 93 |
+
@group(0) @binding(4) var<storage, read_write> O : array<f32>;
|
| 94 |
+
@group(0) @binding(5) var<uniform> dims : vec4<u32>; // m, kw, n, flags
|
| 95 |
+
@compute @workgroup_size(8, 8, 1)
|
| 96 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 97 |
+
let m = dims.x; let kw = dims.y; let n = dims.z;
|
| 98 |
+
let row = gid.x; let col = gid.y; let bz = gid.z;
|
| 99 |
+
if (row >= m || col >= n) { return; }
|
| 100 |
+
var s : i32 = 0;
|
| 101 |
+
let xo = (bz * m + row) * kw;
|
| 102 |
+
let wo = (bz * n + col) * kw;
|
| 103 |
+
for (var p = 0u; p < kw; p = p + 1u) {
|
| 104 |
+
s = s + dot4I8Packed(Xp[xo + p], Wp[wo + p]);
|
| 105 |
+
}
|
| 106 |
+
var v = f32(s) * rs[bz * m + row] * cs[bz * n + col];
|
| 107 |
+
if ((dims.w & 1u) == 1u && v < 0.0) { v = 0.0; }
|
| 108 |
+
O[(bz * m + row) * n + col] = v;
|
| 109 |
+
}`;
|
| 110 |
+
|
| 111 |
async function loadLUTs(base) {
|
| 112 |
base = base || "";
|
| 113 |
const [mulB, reqB, reluB, meta] = await Promise.all([
|
|
|
|
| 152 |
const lut32 = new Int32Array(L.mul); // widen int16 -> int32
|
| 153 |
const lutBuf = device.createBuffer({ size: lut32.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
| 154 |
device.queue.writeBuffer(lutBuf, 0, lut32);
|
| 155 |
+
const bgLutModule = device.createShaderModule({ code: WGSL_BG_LUT });
|
| 156 |
+
const bgLutPipe = device.createComputePipeline({ layout: "auto", compute: { module: bgLutModule, entryPoint: "main" } });
|
| 157 |
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
|
| 158 |
+
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
|
| 159 |
+
bgemm: (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, bgLutPipe, lutBuf, Xq, Wq, rs, cs, d) };
|
| 160 |
|
| 161 |
// DP4A pipeline — only if the WGSL feature exists AND it reproduces the
|
| 162 |
// verified units exactly on random self-tests
|
|
|
|
| 175 |
for (let i = 0; i < ref.length; i++)
|
| 176 |
if (hw[i] !== ref[i]) { console.warn("DP4A disagreed with the verified units — using LUT shader"); return viaLUT; }
|
| 177 |
}
|
| 178 |
+
// fused/batched DP4A kernel, gated by the same verification: it must
|
| 179 |
+
// reproduce the CPU LUT mirror bit-for-bit on the integer accumulate
|
| 180 |
+
const bgDp4Module = device.createShaderModule({ code: WGSL_BG_DP4 });
|
| 181 |
+
const bgDp4Pipe = device.createComputePipeline({ layout: "auto", compute: { module: bgDp4Module, entryPoint: "main" } });
|
| 182 |
+
const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, bgDp4Pipe, Xq, Wq, rs, cs, d);
|
| 183 |
+
{
|
| 184 |
+
const d0 = { m: 5, k: 9, n: 6, batch: 3, relu: true };
|
| 185 |
+
const Xq = new Int8Array(d0.batch * d0.m * d0.k), Wq = new Int8Array(d0.batch * d0.k * d0.n);
|
| 186 |
+
for (let i = 0; i < Xq.length; i++) Xq[i] = (Math.random() * 256 - 128) | 0;
|
| 187 |
+
for (let i = 0; i < Wq.length; i++) Wq[i] = (Math.random() * 256 - 128) | 0;
|
| 188 |
+
const rs = new Float32Array(d0.batch * d0.m).fill(0).map(() => Math.random() + 0.5);
|
| 189 |
+
const cs = new Float32Array(d0.batch * d0.n).fill(0).map(() => Math.random() + 0.5);
|
| 190 |
+
const hw = await bg(Xq, Wq, rs, cs, d0);
|
| 191 |
+
const ref = root.Verified.bgemmJS(Xq, Wq, rs, cs, d0, L);
|
| 192 |
+
for (let i = 0; i < ref.length; i++)
|
| 193 |
+
if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-6 + 1e-6) {
|
| 194 |
+
console.warn("batched DP4A disagreed with the verified units — using LUT bgemm");
|
| 195 |
+
return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
|
| 196 |
+
matmulInt8: dp4mm, bgemm: viaLUT.bgemm };
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot — HW verified vs units)`,
|
| 200 |
+
matmulInt8: dp4mm, bgemm: bg };
|
| 201 |
} catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
|
| 202 |
}
|
| 203 |
|
|
|
|
| 247 |
return r.out;
|
| 248 |
}
|
| 249 |
|
| 250 |
+
// fused batched dispatch: f32 out, epilogue done on-device
|
| 251 |
+
async function runBgPass(device, pipeline, entries, m, n, batch) {
|
| 252 |
+
const bytesO = batch * m * n * 4;
|
| 253 |
+
const bufO = mk(device, bytesO, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 254 |
+
const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: entries(bufO) });
|
| 255 |
+
const enc = device.createCommandEncoder();
|
| 256 |
+
const pass = enc.beginComputePass();
|
| 257 |
+
pass.setPipeline(pipeline); pass.setBindGroup(0, bind);
|
| 258 |
+
pass.dispatchWorkgroups(Math.ceil(m / 8), Math.ceil(n / 8), batch); pass.end();
|
| 259 |
+
const read = mk(device, bytesO, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
|
| 260 |
+
enc.copyBufferToBuffer(bufO, 0, read, 0, bytesO);
|
| 261 |
+
device.queue.submit([enc.finish()]);
|
| 262 |
+
await read.mapAsync(GPUMapMode.READ);
|
| 263 |
+
const out = new Float32Array(read.getMappedRange().slice(0));
|
| 264 |
+
read.unmap();
|
| 265 |
+
return { out, bufO, read };
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
async function gpuBgemmLUT(device, pipeline, lutBuf, Xq, Wq, rs, cs, d) {
|
| 269 |
+
const { m, k, n } = d, batch = d.batch || 1, flags = d.relu ? 1 : 0;
|
| 270 |
+
const bufX = up(device, Int32Array.from(Xq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 271 |
+
const bufW = up(device, Int32Array.from(Wq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 272 |
+
const bufR = up(device, rs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 273 |
+
const bufS = up(device, cs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 274 |
+
const bufD = up(device, new Uint32Array([m, k, n, flags]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 275 |
+
const r = await runBgPass(device, pipeline, (bufO) => [
|
| 276 |
+
{ binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
|
| 277 |
+
{ binding: 2, resource: { buffer: lutBuf } }, { binding: 3, resource: { buffer: bufR } },
|
| 278 |
+
{ binding: 4, resource: { buffer: bufS } }, { binding: 5, resource: { buffer: bufO } },
|
| 279 |
+
{ binding: 6, resource: { buffer: bufD } } ], m, n, batch);
|
| 280 |
+
[bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
|
| 281 |
+
return r.out;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
async function gpuBgemmDP4(device, pipeline, Xq, Wq, rs, cs, d) {
|
| 285 |
+
const { m, k, n } = d, batch = d.batch || 1, flags = d.relu ? 1 : 0;
|
| 286 |
+
const kw = Math.ceil(k / 4);
|
| 287 |
+
const Xp = packRows(Xq, batch * m, k, kw); // rows are (batch·m)
|
| 288 |
+
const Wp = new Uint32Array(batch * n * kw); // per-batch Wᵀ, packed
|
| 289 |
+
for (let bz = 0; bz < batch; bz++) {
|
| 290 |
+
const wt = transposeI8(Wq.subarray(bz * k * n, (bz + 1) * k * n), k, n);
|
| 291 |
+
Wp.set(packRows(wt, n, k, kw), bz * n * kw);
|
| 292 |
+
}
|
| 293 |
+
const bufX = up(device, Xp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 294 |
+
const bufW = up(device, Wp, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 295 |
+
const bufR = up(device, rs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 296 |
+
const bufS = up(device, cs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 297 |
+
const bufD = up(device, new Uint32Array([m, kw, n, flags]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 298 |
+
const r = await runBgPass(device, pipeline, (bufO) => [
|
| 299 |
+
{ binding: 0, resource: { buffer: bufX } }, { binding: 1, resource: { buffer: bufW } },
|
| 300 |
+
{ binding: 2, resource: { buffer: bufR } }, { binding: 3, resource: { buffer: bufS } },
|
| 301 |
+
{ binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], m, n, batch);
|
| 302 |
+
[bufX, bufW, bufR, bufS, bufD, r.bufO, r.read].forEach(b => b.destroy());
|
| 303 |
+
return r.out;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
function mk(device, size, usage) { return device.createBuffer({ size, usage }); }
|
| 307 |
function up(device, arr, usage) {
|
| 308 |
const b = mk(device, Math.max(16, arr.byteLength), usage);
|