Web demo: split-K backward + gather-fused attention
Browse files- web/public/transformer.js +36 -40
- web/public/verified_core.js +67 -2
- web/public/webgpu.js +197 -3
web/public/transformer.js
CHANGED
|
@@ -119,10 +119,6 @@
|
|
| 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) -------------------------------------------------
|
| 128 |
function lnFwd(x, rows, C) {
|
|
@@ -158,7 +154,8 @@
|
|
| 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,
|
|
@@ -218,26 +215,16 @@
|
|
| 218 |
const k = await vmm(l1.y, bl.Wk, BT, C, C, ctx);
|
| 219 |
const v = await vmm(l1.y, bl.Wv, BT, C, C, ctx);
|
| 220 |
cb.q = q; cb.k = k; cb.v = v;
|
| 221 |
-
const ctxOut = new Float32Array(BT * C);
|
| 222 |
-
cb.att = []; // per (b,h): softmax probs
|
| 223 |
const scale = 1 / Math.sqrt(hd);
|
| 224 |
-
// gather
|
| 225 |
-
//
|
| 226 |
-
//
|
| 227 |
-
|
| 228 |
-
const
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
const src = (bi * T + ti) * C + h * hd + j;
|
| 234 |
-
qb[(bz * T + ti) * hd + j] = q[src];
|
| 235 |
-
kb[(bz * T + ti) * hd + j] = k[src];
|
| 236 |
-
vb[(bz * T + ti) * hd + j] = v[src];
|
| 237 |
-
ktb[(bz * hd + j) * T + ti] = k[src]; // kα΅ per head, for scores
|
| 238 |
-
}
|
| 239 |
-
}
|
| 240 |
-
const sAll = await vmmBatched(qb, ktb, T, hd, T, BH, ctx); // scores through the units
|
| 241 |
const aAll = new Float32Array(BH * T * T); // causal softmax
|
| 242 |
for (let bz = 0; bz < BH; bz++) {
|
| 243 |
const so = bz * T * T;
|
|
@@ -249,16 +236,11 @@
|
|
| 249 |
for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
|
| 250 |
}
|
| 251 |
}
|
| 252 |
-
const
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
cb.att.push({ a: aAll.subarray(bz * T * T, (bz + 1) * T * T),
|
| 258 |
-
qh: qb.subarray(bz * T * hd, (bz + 1) * T * hd),
|
| 259 |
-
kh: kb.subarray(bz * T * hd, (bz + 1) * T * hd),
|
| 260 |
-
vh: vb.subarray(bz * T * hd, (bz + 1) * T * hd) });
|
| 261 |
-
}
|
| 262 |
cb.ctxOut = ctxOut;
|
| 263 |
const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
|
| 264 |
const x2 = new Float32Array(BT * C);
|
|
@@ -295,15 +277,26 @@
|
|
| 295 |
}
|
| 296 |
|
| 297 |
// ---- STE backward (float), mirrors forward exactly --------------------------
|
| 298 |
-
|
|
|
|
|
|
|
| 299 |
const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
|
| 300 |
const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
|
| 301 |
const g = m.params.map(p => new Float32Array(p.length));
|
| 302 |
const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
|
| 303 |
// tied unembed: logits = lnf @ embα΅, so the unembedding gradient flows
|
| 304 |
// straight into emb β dlogitsα΅ @ lnf is VΓC, emb's own shape
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
const scale = 1 / Math.sqrt(hd);
|
| 308 |
for (let l = layers - 1; l >= 0; l--) {
|
| 309 |
const bl = m.blocks[l], cb = cache.blocks[l];
|
|
@@ -320,9 +313,12 @@
|
|
| 320 |
g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
|
| 321 |
const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
|
| 322 |
const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
|
| 323 |
-
let ai = 0;
|
| 324 |
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 325 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
const dch = headSlice(dctx, bi, h, T, C, hd); // TΓhd
|
| 327 |
const dvh = mm(tr(a, T, T), dch, T, T, hd); // aα΅ @ dctx
|
| 328 |
const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vα΅
|
|
@@ -364,7 +360,7 @@
|
|
| 364 |
async function trainStep(m) {
|
| 365 |
const { X, Y } = sampleBatch(m.cfg);
|
| 366 |
const { loss, cache } = await forward(m, X, Y);
|
| 367 |
-
const grad = backward(m, cache);
|
| 368 |
return { loss, grad };
|
| 369 |
}
|
| 370 |
|
|
|
|
| 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 |
|
| 123 |
// ---- layernorm (no affine) -------------------------------------------------
|
| 124 |
function lnFwd(x, rows, C) {
|
|
|
|
| 154 |
const add = (name, w) => { params.push(w); names.push(name); return w; };
|
| 155 |
const m = {
|
| 156 |
cfg: { ...cfg, layers, heads, hidden, vocab: vocabSize() },
|
| 157 |
+
ctx: { L, bgemm: (engine && engine.bgemm) || null,
|
| 158 |
+
att: (engine && engine.att) || null, fgemm: (engine && engine.fgemm) || null },
|
| 159 |
emb: add("emb", mk(vocabSize() * c, 0.08)),
|
| 160 |
pos: add("pos", mk(cfg.t * c, 0.02)),
|
| 161 |
blocks: [], params, names,
|
|
|
|
| 215 |
const k = await vmm(l1.y, bl.Wk, BT, C, C, ctx);
|
| 216 |
const v = await vmm(l1.y, bl.Wv, BT, C, C, ctx);
|
| 217 |
cb.q = q; cb.k = k; cb.v = v;
|
|
|
|
|
|
|
| 218 |
const scale = 1 / Math.sqrt(hd);
|
| 219 |
+
// gather-FUSED attention (CUTLASS ex. 36/52): the kernels read q/k/v in
|
| 220 |
+
// their natural BTΓC layout with head-strided indexing and scatter ctx
|
| 221 |
+
// straight back β no JS gather copies, no kα΅ transpose. All BΓH heads in
|
| 222 |
+
// one dispatch per stage, every product through the verified units.
|
| 223 |
+
const BH = B * heads, dAtt = { B, T, heads, hd };
|
| 224 |
+
// per-(token,head) row quantization: the (BTΒ·heads)Γhd view IS the buffer
|
| 225 |
+
const qq = V.quantizeRows(q, BT * heads, hd), kq = V.quantizeRows(k, BT * heads, hd);
|
| 226 |
+
const sAll = ctx.att ? await ctx.att.scores(qq.q, kq.q, qq.s, kq.s, dAtt)
|
| 227 |
+
: V.attScoresJS(qq.q, kq.q, qq.s, kq.s, dAtt, ctx.L);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
const aAll = new Float32Array(BH * T * T); // causal softmax
|
| 229 |
for (let bz = 0; bz < BH; bz++) {
|
| 230 |
const so = bz * T * T;
|
|
|
|
| 236 |
for (let tj = 0; tj <= ti; tj++) aAll[so + ti * T + tj] /= z;
|
| 237 |
}
|
| 238 |
}
|
| 239 |
+
const aq = V.quantizeRows(aAll, BH * T, T);
|
| 240 |
+
const vq = V.quantizeHeadCols(v, B, T, heads, hd);
|
| 241 |
+
const ctxOut = ctx.att ? await ctx.att.ctx(aq.q, vq.q, aq.s, vq.s, dAtt)
|
| 242 |
+
: V.attCtxJS(aq.q, vq.q, aq.s, vq.s, dAtt, ctx.L);
|
| 243 |
+
cb.aAll = aAll; // backward slices heads from q/k/v/aAll
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
cb.ctxOut = ctxOut;
|
| 245 |
const attnOut = await vmm(ctxOut, bl.Wo, BT, C, C, ctx);
|
| 246 |
const x2 = new Float32Array(BT * C);
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
// ---- STE backward (float), mirrors forward exactly --------------------------
|
| 280 |
+
// The two vocab-sized matmuls run on the split-K f32 GPU kernel when
|
| 281 |
+
// available (CUTLASS ex. 06) β same float math, off the JS thread.
|
| 282 |
+
async function backward(m, cache) {
|
| 283 |
const { c: C, t: T, b: B, layers, heads, hidden, vocab } = m.cfg;
|
| 284 |
const BT = B * T, hd = C / heads, mm = TC.matmul, tr = TC.transpose;
|
| 285 |
const g = m.params.map(p => new Float32Array(p.length));
|
| 286 |
const gi = Object.fromEntries(m.names.map((n, i) => [n, i]));
|
| 287 |
// tied unembed: logits = lnf @ embα΅, so the unembedding gradient flows
|
| 288 |
// straight into emb β dlogitsα΅ @ lnf is VΓC, emb's own shape
|
| 289 |
+
let dlnfIn;
|
| 290 |
+
if (m.ctx.fgemm) {
|
| 291 |
+
[g[gi.emb], dlnfIn] = await Promise.all([
|
| 292 |
+
m.ctx.fgemm(cache.dlogits, cache.lnf.y, { m: vocab, k: BT, n: C, transA: true }),
|
| 293 |
+
m.ctx.fgemm(cache.dlogits, m.emb, { m: BT, k: vocab, n: C }), // split-K shape
|
| 294 |
+
]);
|
| 295 |
+
} else {
|
| 296 |
+
g[gi.emb] = mm(tr(cache.dlogits, BT, vocab), cache.lnf.y, vocab, BT, C);
|
| 297 |
+
dlnfIn = mm(cache.dlogits, m.emb, BT, vocab, C);
|
| 298 |
+
}
|
| 299 |
+
let dx = lnBwd(dlnfIn, cache.lnf.y, cache.lnf.sig, BT, C);
|
| 300 |
const scale = 1 / Math.sqrt(hd);
|
| 301 |
for (let l = layers - 1; l >= 0; l--) {
|
| 302 |
const bl = m.blocks[l], cb = cache.blocks[l];
|
|
|
|
| 313 |
g[gi[`b${l}.Wo`]] = mm(tr(cb.ctxOut, BT, C), dx2, C, BT, C);
|
| 314 |
const dctx = mm(dx2, tr(bl.Wo, C, C), BT, C, C);
|
| 315 |
const dq = new Float32Array(BT * C), dk = new Float32Array(BT * C), dv = new Float32Array(BT * C);
|
|
|
|
| 316 |
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 317 |
+
const bz = bi * heads + h;
|
| 318 |
+
const a = cb.aAll.subarray(bz * T * T, (bz + 1) * T * T);
|
| 319 |
+
const qh = headSlice(cb.q, bi, h, T, C, hd);
|
| 320 |
+
const kh = headSlice(cb.k, bi, h, T, C, hd);
|
| 321 |
+
const vh = headSlice(cb.v, bi, h, T, C, hd);
|
| 322 |
const dch = headSlice(dctx, bi, h, T, C, hd); // TΓhd
|
| 323 |
const dvh = mm(tr(a, T, T), dch, T, T, hd); // aα΅ @ dctx
|
| 324 |
const da = mm(dch, tr(vh, T, hd), T, hd, T); // dctx @ vα΅
|
|
|
|
| 360 |
async function trainStep(m) {
|
| 361 |
const { X, Y } = sampleBatch(m.cfg);
|
| 362 |
const { loss, cache } = await forward(m, X, Y);
|
| 363 |
+
const grad = await backward(m, cache);
|
| 364 |
return { loss, grad };
|
| 365 |
}
|
| 366 |
|
web/public/verified_core.js
CHANGED
|
@@ -147,6 +147,71 @@
|
|
| 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,8 +262,8 @@
|
|
| 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);
|
|
|
|
| 147 |
return bgemmJS(x.q, wq, x.s, ws, d, L);
|
| 148 |
}
|
| 149 |
|
| 150 |
+
// ---- gather-fused attention through the units (CUTLASS ex. 36/52) ----------
|
| 151 |
+
// The kernels read q/k/v/ctx directly in their natural BTΓC layout with
|
| 152 |
+
// head-strided indexing β no JS gather copies, no kα΅ transpose, and the
|
| 153 |
+
// context write scatters straight back into BTΓC. Quantization stays
|
| 154 |
+
// block-scaled: q/k/a per (token,head) row, v per (head,channel) column.
|
| 155 |
+
// The (BTΒ·heads)Γhd row view of q/k IS the contiguous buffer, so
|
| 156 |
+
// quantizeRows(q, BTΒ·heads, hd) gives per-(token,head) scales for free.
|
| 157 |
+
function quantizeHeadCols(v, B, T, heads, hd) { // per (batch,head,channel) column
|
| 158 |
+
const C = heads * hd;
|
| 159 |
+
const q = new Int8Array(B * T * C), s = new Float32Array(B * heads * hd);
|
| 160 |
+
for (let bi = 0; bi < B; bi++)
|
| 161 |
+
for (let h = 0; h < heads; h++)
|
| 162 |
+
for (let j = 0; j < hd; j++) {
|
| 163 |
+
let mx = 0;
|
| 164 |
+
for (let ti = 0; ti < T; ti++) {
|
| 165 |
+
const a = Math.abs(v[(bi * T + ti) * C + h * hd + j]);
|
| 166 |
+
if (a > mx) mx = a;
|
| 167 |
+
}
|
| 168 |
+
const sc = Math.max(mx / 127, 1e-8);
|
| 169 |
+
s[(bi * heads + h) * hd + j] = sc;
|
| 170 |
+
for (let ti = 0; ti < T; ti++) {
|
| 171 |
+
const idx = (bi * T + ti) * C + h * hd + j;
|
| 172 |
+
const w = Math.round(v[idx] / sc);
|
| 173 |
+
q[idx] = w < -128 ? -128 : w > 127 ? 127 : w;
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
return { q, s };
|
| 177 |
+
}
|
| 178 |
+
// scores S[bz,ti,tj] = q_row(bi,ti,h) Β· k_row(bi,tj,h), every product via the LUT
|
| 179 |
+
function attScoresJS(qq, kq, qs, ks, d, L) {
|
| 180 |
+
const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul;
|
| 181 |
+
const out = new Float32Array(B * heads * T * T);
|
| 182 |
+
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 183 |
+
const bz = bi * heads + h;
|
| 184 |
+
for (let ti = 0; ti < T; ti++) {
|
| 185 |
+
const qo = (bi * T + ti) * C + h * hd, rscale = qs[(bi * T + ti) * heads + h];
|
| 186 |
+
for (let tj = 0; tj < T; tj++) {
|
| 187 |
+
const ko = (bi * T + tj) * C + h * hd;
|
| 188 |
+
let acc = 0;
|
| 189 |
+
for (let p = 0; p < hd; p++) acc += mul[(qq[qo + p] & 0xFF) * 256 + (kq[ko + p] & 0xFF)];
|
| 190 |
+
out[(bz * T + ti) * T + tj] = acc * rscale * ks[(bi * T + tj) * heads + h];
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
return out;
|
| 195 |
+
}
|
| 196 |
+
// ctx[(bi,ti),(h,j)] = Ξ£_tj a[bz,ti,tj]Β·v[(bi,tj),(h,j)] β scatter fused into BTΓC
|
| 197 |
+
function attCtxJS(aq, vq, as, vs, d, L) {
|
| 198 |
+
const { B, T, heads, hd } = d, C = heads * hd, mul = L.mul;
|
| 199 |
+
const out = new Float32Array(B * T * C);
|
| 200 |
+
for (let bi = 0; bi < B; bi++) for (let h = 0; h < heads; h++) {
|
| 201 |
+
const bz = bi * heads + h;
|
| 202 |
+
for (let ti = 0; ti < T; ti++) {
|
| 203 |
+
const ao = (bz * T + ti) * T, rscale = as[bz * T + ti];
|
| 204 |
+
for (let j = 0; j < hd; j++) {
|
| 205 |
+
let acc = 0;
|
| 206 |
+
for (let tj = 0; tj < T; tj++)
|
| 207 |
+
acc += mul[(aq[ao + tj] & 0xFF) * 256 + (vq[(bi * T + tj) * C + h * hd + j] & 0xFF)];
|
| 208 |
+
out[(bi * T + ti) * C + h * hd + j] = acc * rscale * vs[(bi * heads + h) * hd + j];
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
return out;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
// one verified layer forward; returns float out (+ cache for STE backward).
|
| 216 |
// Every product goes through the verified INT8 multiply (mul8 LUT) with exact
|
| 217 |
// int32 accumulation β i.e. an emulated INT8 tensor-core GEMM β then dequant.
|
|
|
|
| 262 |
for (let j = 0; j < W2.length; j++) W2[j] -= lr * gAvg[W1.length + j];
|
| 263 |
}
|
| 264 |
|
| 265 |
+
const api = { quantize, quantize2, quantizeRows, quantizeCols, quantizeHeadCols, lutMatmulJS, lutMatmul3JS, lutMatmul3,
|
| 266 |
+
bgemmJS, vgemmBlock, attScoresJS, attCtxJS, linearFwd, forward, backward, splitApply };
|
| 267 |
if (typeof module !== "undefined" && module.exports) { TC = require("./traincore.js"); module.exports = api; }
|
| 268 |
else { TC = root.TrainCore; root.Verified = api; }
|
| 269 |
})(typeof self !== "undefined" ? self : this);
|
web/public/webgpu.js
CHANGED
|
@@ -108,6 +108,91 @@
|
|
| 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([
|
|
@@ -154,9 +239,55 @@
|
|
| 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
|
|
@@ -193,11 +324,11 @@
|
|
| 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,6 +378,69 @@
|
|
| 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;
|
|
|
|
| 108 |
O[(bz * m + row) * n + col] = v;
|
| 109 |
}`;
|
| 110 |
|
| 111 |
+
// Gather-fused attention (CUTLASS ex. 36/52): kernels index q/k/v directly in
|
| 112 |
+
// their BTΓC layout (head-strided) β no gather copies, no kα΅ transpose β and
|
| 113 |
+
// the ctx kernel scatters straight back into BTΓC. int8Γint8βi32 is exact, so
|
| 114 |
+
// these are bit-identical to the LUT mirrors (proved at init before use).
|
| 115 |
+
const WGSL_ATT_SCORES = `
|
| 116 |
+
@group(0) @binding(0) var<storage, read> Q : array<i32>; // int8 per elem, BTΓC
|
| 117 |
+
@group(0) @binding(1) var<storage, read> K : array<i32>;
|
| 118 |
+
@group(0) @binding(2) var<storage, read> qs : array<f32>; // per (token,head)
|
| 119 |
+
@group(0) @binding(3) var<storage, read> ks : array<f32>;
|
| 120 |
+
@group(0) @binding(4) var<storage, read_write> O : array<f32>;
|
| 121 |
+
@group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
|
| 122 |
+
@compute @workgroup_size(8, 8, 1)
|
| 123 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 124 |
+
let T = dims.x; let heads = dims.y; let hd = dims.z;
|
| 125 |
+
let ti = gid.x; let tj = gid.y; let bz = gid.z;
|
| 126 |
+
if (ti >= T || tj >= T) { return; }
|
| 127 |
+
let bi = bz / heads; let h = bz % heads;
|
| 128 |
+
let C = heads * hd;
|
| 129 |
+
let qo = (bi * T + ti) * C + h * hd;
|
| 130 |
+
let ko = (bi * T + tj) * C + h * hd;
|
| 131 |
+
var s : i32 = 0;
|
| 132 |
+
for (var p = 0u; p < hd; p = p + 1u) { s = s + Q[qo + p] * K[ko + p]; }
|
| 133 |
+
O[(bz * T + ti) * T + tj] = f32(s) * qs[(bi * T + ti) * heads + h] * ks[(bi * T + tj) * heads + h];
|
| 134 |
+
}`;
|
| 135 |
+
const WGSL_ATT_CTX = `
|
| 136 |
+
@group(0) @binding(0) var<storage, read> A : array<i32>; // int8, BHΓTΓT
|
| 137 |
+
@group(0) @binding(1) var<storage, read> V : array<i32>; // int8, BTΓC
|
| 138 |
+
@group(0) @binding(2) var<storage, read> as_ : array<f32>; // per (bz,row)
|
| 139 |
+
@group(0) @binding(3) var<storage, read> vs : array<f32>; // per (batch,head,chan)
|
| 140 |
+
@group(0) @binding(4) var<storage, read_write> O : array<f32>; // BTΓC (scatter fused)
|
| 141 |
+
@group(0) @binding(5) var<uniform> dims : vec4<u32>; // T, heads, hd, _
|
| 142 |
+
@compute @workgroup_size(8, 8, 1)
|
| 143 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 144 |
+
let T = dims.x; let heads = dims.y; let hd = dims.z;
|
| 145 |
+
let ti = gid.x; let j = gid.y; let bz = gid.z;
|
| 146 |
+
if (ti >= T || j >= hd) { return; }
|
| 147 |
+
let bi = bz / heads; let h = bz % heads;
|
| 148 |
+
let C = heads * hd;
|
| 149 |
+
let ao = (bz * T + ti) * T;
|
| 150 |
+
var s : i32 = 0;
|
| 151 |
+
for (var tj = 0u; tj < T; tj = tj + 1u) { s = s + A[ao + tj] * V[(bi * T + tj) * C + h * hd + j]; }
|
| 152 |
+
O[(bi * T + ti) * C + h * hd + j] = f32(s) * as_[bz * T + ti] * vs[(bi * heads + h) * hd + j];
|
| 153 |
+
}`;
|
| 154 |
+
|
| 155 |
+
// Split-K f32 GEMM (CUTLASS ex. 06) for the STE BACKWARD only β the backward
|
| 156 |
+
// was always float (the integer path has no gradient); this just moves that
|
| 157 |
+
// exact float math off the JS thread. Split-K matters for dlnf: M=256, N=32,
|
| 158 |
+
// K=16512 β 8k outputs with a huge inner loop would idle the GPU, so slices
|
| 159 |
+
// of K run on separate workgroups and a second tiny pass reduces partials.
|
| 160 |
+
const WGSL_FGEMM = `
|
| 161 |
+
@group(0) @binding(0) var<storage, read> A : array<f32>;
|
| 162 |
+
@group(0) @binding(1) var<storage, read> Bm : array<f32>;
|
| 163 |
+
@group(0) @binding(2) var<storage, read_write> P : array<f32>; // S partials
|
| 164 |
+
@group(0) @binding(3) var<uniform> dims : vec4<u32>; // m, k, n, flags(bit0=transA, rest=S)
|
| 165 |
+
@compute @workgroup_size(8, 8, 1)
|
| 166 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 167 |
+
let m = dims.x; let k = dims.y; let n = dims.z;
|
| 168 |
+
let transA = (dims.w & 1u) == 1u;
|
| 169 |
+
let S = dims.w >> 1u;
|
| 170 |
+
let row = gid.x; let col = gid.y; let z = gid.z;
|
| 171 |
+
if (row >= m || col >= n) { return; }
|
| 172 |
+
let ks = (k + S - 1u) / S;
|
| 173 |
+
let p0 = z * ks;
|
| 174 |
+
let p1 = min(k, p0 + ks);
|
| 175 |
+
var s : f32 = 0.0;
|
| 176 |
+
for (var p = p0; p < p1; p = p + 1u) {
|
| 177 |
+
let a = select(A[row * k + p], A[p * m + row], transA);
|
| 178 |
+
s = s + a * Bm[p * n + col];
|
| 179 |
+
}
|
| 180 |
+
P[(z * m + row) * n + col] = s;
|
| 181 |
+
}`;
|
| 182 |
+
const WGSL_FREDUCE = `
|
| 183 |
+
@group(0) @binding(0) var<storage, read> P : array<f32>;
|
| 184 |
+
@group(0) @binding(1) var<storage, read_write> O : array<f32>;
|
| 185 |
+
@group(0) @binding(2) var<uniform> dims : vec4<u32>; // mn, S, _, _
|
| 186 |
+
@compute @workgroup_size(64)
|
| 187 |
+
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
|
| 188 |
+
let mn = dims.x; let S = dims.y;
|
| 189 |
+
let i = gid.x;
|
| 190 |
+
if (i >= mn) { return; }
|
| 191 |
+
var s : f32 = 0.0;
|
| 192 |
+
for (var z = 0u; z < S; z = z + 1u) { s = s + P[z * mn + i]; }
|
| 193 |
+
O[i] = s;
|
| 194 |
+
}`;
|
| 195 |
+
|
| 196 |
async function loadLUTs(base) {
|
| 197 |
base = base || "";
|
| 198 |
const [mulB, reqB, reluB, meta] = await Promise.all([
|
|
|
|
| 239 |
device.queue.writeBuffer(lutBuf, 0, lut32);
|
| 240 |
const bgLutModule = device.createShaderModule({ code: WGSL_BG_LUT });
|
| 241 |
const bgLutPipe = device.createComputePipeline({ layout: "auto", compute: { module: bgLutModule, entryPoint: "main" } });
|
| 242 |
+
|
| 243 |
+
// gather-fused attention kernels β must reproduce the LUT mirrors exactly
|
| 244 |
+
const mkPipe = (code) => device.createComputePipeline({ layout: "auto",
|
| 245 |
+
compute: { module: device.createShaderModule({ code }), entryPoint: "main" } });
|
| 246 |
+
const scoresPipe = mkPipe(WGSL_ATT_SCORES), ctxPipe = mkPipe(WGSL_ATT_CTX);
|
| 247 |
+
let att = { scores: (qq, kq, qs, ks, d) => gpuAttScores(device, scoresPipe, qq, kq, qs, ks, d),
|
| 248 |
+
ctx: (aq, vq, as, vs, d) => gpuAttCtx(device, ctxPipe, aq, vq, as, vs, d) };
|
| 249 |
+
try {
|
| 250 |
+
const d0 = { B: 2, T: 8, heads: 2, hd: 8 };
|
| 251 |
+
const nQ = d0.B * d0.T * d0.heads * d0.hd;
|
| 252 |
+
const qq = new Int8Array(nQ), kq = new Int8Array(nQ), vq = new Int8Array(nQ);
|
| 253 |
+
for (let i = 0; i < nQ; i++) { qq[i] = (Math.random() * 256 - 128) | 0; kq[i] = (Math.random() * 256 - 128) | 0; vq[i] = (Math.random() * 256 - 128) | 0; }
|
| 254 |
+
const qs = Float32Array.from({ length: d0.B * d0.T * d0.heads }, () => Math.random() + 0.5);
|
| 255 |
+
const ks = Float32Array.from(qs, () => Math.random() + 0.5);
|
| 256 |
+
const aq = new Int8Array(d0.B * d0.heads * d0.T * d0.T);
|
| 257 |
+
for (let i = 0; i < aq.length; i++) aq[i] = (Math.random() * 127) | 0;
|
| 258 |
+
const as = Float32Array.from({ length: d0.B * d0.heads * d0.T }, () => Math.random() + 0.5);
|
| 259 |
+
const vs = Float32Array.from({ length: d0.B * d0.heads * d0.hd }, () => Math.random() + 0.5);
|
| 260 |
+
const [hwS, hwC] = await Promise.all([att.scores(qq, kq, qs, ks, d0), att.ctx(aq, vq, as, vs, d0)]);
|
| 261 |
+
const refS = root.Verified.attScoresJS(qq, kq, qs, ks, d0, L);
|
| 262 |
+
const refC = root.Verified.attCtxJS(aq, vq, as, vs, d0, L);
|
| 263 |
+
const close = (a, b) => Math.abs(a - b) <= Math.abs(b) * 1e-6 + 1e-6;
|
| 264 |
+
for (let i = 0; i < refS.length; i++) if (!close(hwS[i], refS[i])) throw new Error("scores mismatch");
|
| 265 |
+
for (let i = 0; i < refC.length; i++) if (!close(hwC[i], refC[i])) throw new Error("ctx mismatch");
|
| 266 |
+
} catch (e) {
|
| 267 |
+
console.warn("fused attention kernels failed verification β using CPU LUT mirrors:", e.message);
|
| 268 |
+
att = null;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// split-K f32 GEMM for the STE backward (self-tested vs JS float matmul)
|
| 272 |
+
const fPipes = { gemm: mkPipe(WGSL_FGEMM), reduce: mkPipe(WGSL_FREDUCE) };
|
| 273 |
+
let fgemm = (A, Bm, d) => gpuFgemm(device, fPipes, A, Bm, d);
|
| 274 |
+
try {
|
| 275 |
+
const m0 = 7, k0 = 4500, n0 = 5; // k big enough to exercise split-K
|
| 276 |
+
const A = Float32Array.from({ length: m0 * k0 }, () => Math.random() - 0.5);
|
| 277 |
+
const Bm = Float32Array.from({ length: k0 * n0 }, () => Math.random() - 0.5);
|
| 278 |
+
const hw = await fgemm(A, Bm, { m: m0, k: k0, n: n0 });
|
| 279 |
+
const ref = root.TrainCore.matmul(A, Bm, m0, k0, n0);
|
| 280 |
+
for (let i = 0; i < ref.length; i++)
|
| 281 |
+
if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-3 + 1e-3) throw new Error("fgemm mismatch");
|
| 282 |
+
} catch (e) {
|
| 283 |
+
console.warn("split-K f32 GEMM failed verification β backward stays in JS:", e.message);
|
| 284 |
+
fgemm = null;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader)`,
|
| 288 |
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
|
| 289 |
+
bgemm: (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, bgLutPipe, lutBuf, Xq, Wq, rs, cs, d),
|
| 290 |
+
att, fgemm };
|
| 291 |
|
| 292 |
// DP4A pipeline β only if the WGSL feature exists AND it reproduces the
|
| 293 |
// verified units exactly on random self-tests
|
|
|
|
| 324 |
if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-6 + 1e-6) {
|
| 325 |
console.warn("batched DP4A disagreed with the verified units β using LUT bgemm");
|
| 326 |
return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot β HW verified vs units)`,
|
| 327 |
+
matmulInt8: dp4mm, bgemm: viaLUT.bgemm, att, fgemm };
|
| 328 |
}
|
| 329 |
}
|
| 330 |
return { backend: "webgpu", label: `${gpuName} (DP4A int8 dot β HW verified vs units)`,
|
| 331 |
+
matmulInt8: dp4mm, bgemm: bg, att, fgemm };
|
| 332 |
} catch (e) { console.warn("WebGPU init failed, CPU fallback:", e); return cpu; }
|
| 333 |
}
|
| 334 |
|
|
|
|
| 378 |
return r.out;
|
| 379 |
}
|
| 380 |
|
| 381 |
+
// attention kernels: int8 (widened i32) in, f32 out, strided head indexing
|
| 382 |
+
async function gpuAttScores(device, pipeline, qq, kq, qs, ks, d) {
|
| 383 |
+
const { B, T, heads } = d;
|
| 384 |
+
const bufQ = up(device, Int32Array.from(qq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 385 |
+
const bufK = up(device, Int32Array.from(kq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 386 |
+
const bufQs = up(device, qs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 387 |
+
const bufKs = up(device, ks, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 388 |
+
const bufD = up(device, new Uint32Array([d.T, d.heads, d.hd, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 389 |
+
const r = await runBgPass(device, pipeline, (bufO) => [
|
| 390 |
+
{ binding: 0, resource: { buffer: bufQ } }, { binding: 1, resource: { buffer: bufK } },
|
| 391 |
+
{ binding: 2, resource: { buffer: bufQs } }, { binding: 3, resource: { buffer: bufKs } },
|
| 392 |
+
{ binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, T, B * heads);
|
| 393 |
+
[bufQ, bufK, bufQs, bufKs, bufD, r.bufO, r.read].forEach(b => b.destroy());
|
| 394 |
+
return r.out;
|
| 395 |
+
}
|
| 396 |
+
async function gpuAttCtx(device, pipeline, aq, vq, as, vs, d) {
|
| 397 |
+
const { B, T, heads, hd } = d;
|
| 398 |
+
const bufA = up(device, Int32Array.from(aq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 399 |
+
const bufV = up(device, Int32Array.from(vq), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 400 |
+
const bufAs = up(device, as, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 401 |
+
const bufVs = up(device, vs, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 402 |
+
const bufD = up(device, new Uint32Array([T, heads, hd, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 403 |
+
const r = await runBgPass(device, pipeline, (bufO) => [
|
| 404 |
+
{ binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufV } },
|
| 405 |
+
{ binding: 2, resource: { buffer: bufAs } }, { binding: 3, resource: { buffer: bufVs } },
|
| 406 |
+
{ binding: 4, resource: { buffer: bufO } }, { binding: 5, resource: { buffer: bufD } } ], T, hd, B * heads);
|
| 407 |
+
[bufA, bufV, bufAs, bufVs, bufD, r.bufO, r.read].forEach(b => b.destroy());
|
| 408 |
+
return r.out;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// split-K f32 GEMM (backward): partial pass + reduce pass
|
| 412 |
+
async function gpuFgemm(device, pipes, A, Bm, d) {
|
| 413 |
+
const { m, k, n } = d, transA = d.transA ? 1 : 0;
|
| 414 |
+
const S = k > 4096 ? Math.min(16, Math.ceil(k / 2048)) : 1;
|
| 415 |
+
const bufA = up(device, A, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 416 |
+
const bufB = up(device, Bm, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
|
| 417 |
+
const bufP = mk(device, S * m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 418 |
+
const bufD1 = up(device, new Uint32Array([m, k, n, transA | (S << 1)]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 419 |
+
const bufO = mk(device, m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 420 |
+
const bufD2 = up(device, new Uint32Array([m * n, S, 0, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
|
| 421 |
+
const enc = device.createCommandEncoder();
|
| 422 |
+
let pass = enc.beginComputePass();
|
| 423 |
+
pass.setPipeline(pipes.gemm);
|
| 424 |
+
pass.setBindGroup(0, device.createBindGroup({ layout: pipes.gemm.getBindGroupLayout(0), entries: [
|
| 425 |
+
{ binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufB } },
|
| 426 |
+
{ binding: 2, resource: { buffer: bufP } }, { binding: 3, resource: { buffer: bufD1 } } ] }));
|
| 427 |
+
pass.dispatchWorkgroups(Math.ceil(m / 8), Math.ceil(n / 8), S); pass.end();
|
| 428 |
+
pass = enc.beginComputePass();
|
| 429 |
+
pass.setPipeline(pipes.reduce);
|
| 430 |
+
pass.setBindGroup(0, device.createBindGroup({ layout: pipes.reduce.getBindGroupLayout(0), entries: [
|
| 431 |
+
{ binding: 0, resource: { buffer: bufP } }, { binding: 1, resource: { buffer: bufO } },
|
| 432 |
+
{ binding: 2, resource: { buffer: bufD2 } } ] }));
|
| 433 |
+
pass.dispatchWorkgroups(Math.ceil(m * n / 64)); pass.end();
|
| 434 |
+
const read = mk(device, m * n * 4, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
|
| 435 |
+
enc.copyBufferToBuffer(bufO, 0, read, 0, m * n * 4);
|
| 436 |
+
device.queue.submit([enc.finish()]);
|
| 437 |
+
await read.mapAsync(GPUMapMode.READ);
|
| 438 |
+
const out = new Float32Array(read.getMappedRange().slice(0));
|
| 439 |
+
read.unmap();
|
| 440 |
+
[bufA, bufB, bufP, bufD1, bufO, bufD2, read].forEach(b => b.destroy());
|
| 441 |
+
return out;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
// fused batched dispatch: f32 out, epilogue done on-device
|
| 445 |
async function runBgPass(device, pipeline, entries, m, n, batch) {
|
| 446 |
const bytesO = batch * m * n * 4;
|