Upload web/public/webgpu.js with huggingface_hub
Browse files- web/public/webgpu.js +129 -10
web/public/webgpu.js
CHANGED
|
@@ -363,6 +363,7 @@
|
|
| 363 |
// split-K f32 GEMM for the STE backward (self-tested vs JS float matmul)
|
| 364 |
const fPipes = { gemm: mkPipe(WGSL_FGEMM), reduce: mkPipe(WGSL_FREDUCE) };
|
| 365 |
let fgemm = (A, Bm, d) => gpuFgemm(device, fPipes, A, Bm, d);
|
|
|
|
| 366 |
try {
|
| 367 |
const m0 = 7, k0 = 4500, n0 = 5; // k big enough to exercise split-K
|
| 368 |
const A = Float32Array.from({ length: m0 * k0 }, () => Math.random() - 0.5);
|
|
@@ -371,9 +372,22 @@
|
|
| 371 |
const ref = root.TrainCore.matmul(A, Bm, m0, k0, n0);
|
| 372 |
for (let i = 0; i < ref.length; i++)
|
| 373 |
if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-3 + 1e-3) throw new Error("fgemm mismatch");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
} catch (e) {
|
| 375 |
console.warn("split-K f32 GEMM failed verification β backward stays in JS:", e.message);
|
| 376 |
-
fgemm = null;
|
| 377 |
}
|
| 378 |
|
| 379 |
// Shared exact gate for a bgemm implementation. Sweeps shapes (including
|
|
@@ -404,18 +418,38 @@
|
|
| 404 |
return null;
|
| 405 |
}
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
const bgLut = (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, d.acc ? bgLutVPipe : bgLutPipe, lutBuf, Xq, Wq, rs, cs, d);
|
| 408 |
// the LUT bgemm is the fallback AND the oracle's shader twin β gate it too
|
| 409 |
-
const lutBad = await gateBgemm(bgLut);
|
| 410 |
if (lutBad) { console.warn("LUT bgemm shader failed verification β CPU mirrors only:", lutBad); return cpu; }
|
| 411 |
|
| 412 |
// B2B MLP chain gate (CUTLASS ex. 13+23): run the WHOLE chain β gemm1 +
|
| 413 |
// ReLU + on-GPU rowmax + on-GPU quantize + gemm2 β against the pure-JS
|
| 414 |
// mirror chain, exact `!==` on both h1 and the final output. Sweeps
|
| 415 |
// ragged shapes; h not a multiple of 4 exercises the pack-tail padding.
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
| 419 |
const rnd = (len) => Float32Array.from({ length: len }, () => Math.random() * 2 - 1);
|
| 420 |
const Xf = rnd(d0.m * d0.k), W1 = rnd(d0.k * d0.h), W2 = rnd(d0.h * d0.n);
|
| 421 |
const hw = await root.Verified.vmlpBlock(Xf, W1, W2, d0, L, mlpFn, null);
|
|
@@ -426,6 +460,7 @@
|
|
| 426 |
for (let i = 0; i < ref.out.length; i++)
|
| 427 |
if (bitDiff(hw.out[i], ref.out[i])) return `out mismatch @${i} (${shape}): ${hw.out[i]} vs ${ref.out[i]}`;
|
| 428 |
}
|
|
|
|
| 429 |
// DISCRIMINATING case: the sweep above passes vacuously if no value
|
| 430 |
// lands on a rounding boundary β the old round(x/scale) spec would
|
| 431 |
// pass it too. So hunt (in fast JS) for an input where the two specs
|
|
@@ -467,14 +502,47 @@
|
|
| 467 |
// test_b2b.js: 48M draws targeted at binade edges, 1.9M last-ulp
|
| 468 |
// fused-vs-stepped differences, ZERO floor-visible. The `+0.5` respec is
|
| 469 |
// contraction-immune by construction β `round(x/scale)` was not.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
const lutMlpEnv = { dp4: false, gemm: bgLutPipe, rowmax: rowmaxPipe, quant: quantI32Pipe, lutBuf };
|
| 471 |
let mlpLut = (xq, w1q, w2q, xs, w1s, w2s, d) => gpuMlpChain(device, lutMlpEnv, xq, w1q, w2q, xs, w1s, w2s, d);
|
| 472 |
-
const mlpLutBad = await gateMlp(mlpLut);
|
| 473 |
if (mlpLutBad) { console.warn("B2B MLP chain (LUT) failed verification β MLP stays on the CPU mirror chain:", mlpLutBad); mlpLut = null; }
|
| 474 |
|
| 475 |
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader Β· exact-gated)`,
|
| 476 |
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
|
| 477 |
-
bgemm: bgLut, att, fgemm, mlp: mlpLut };
|
| 478 |
|
| 479 |
// DP4A pipeline β only if the WGSL feature exists AND its batched kernel
|
| 480 |
// reproduces the verified units exactly across the shape sweep
|
|
@@ -482,14 +550,14 @@
|
|
| 482 |
return viaLUT;
|
| 483 |
const bgDp4Pipe = mkPipeL(WGSL_BG_DP4(false), bgDp4Layout), bgDp4VPipe = mkPipeL(WGSL_BG_DP4(true), bgDp4Layout);
|
| 484 |
const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, d.acc ? bgDp4VPipe : bgDp4Pipe, Xq, Wq, rs, cs, d);
|
| 485 |
-
const dp4Bad = await gateBgemm(bg);
|
| 486 |
if (dp4Bad) {
|
| 487 |
console.warn("batched DP4A disagreed with the verified units β using LUT bgemm:", dp4Bad);
|
| 488 |
return viaLUT;
|
| 489 |
}
|
| 490 |
const dp4MlpEnv = { dp4: true, gemm: bgDp4Pipe, rowmax: rowmaxPipe, quant: quantPackPipe };
|
| 491 |
let mlpDp4 = (xq, w1q, w2q, xs, w1s, w2s, d) => gpuMlpChain(device, dp4MlpEnv, xq, w1q, w2q, xs, w1s, w2s, d);
|
| 492 |
-
const mlpDp4Bad = await gateMlp(mlpDp4);
|
| 493 |
if (mlpDp4Bad) { console.warn("B2B MLP chain (DP4A) failed verification β using the LUT chain:", mlpDp4Bad); mlpDp4 = mlpLut; }
|
| 494 |
|
| 495 |
// Both backends are exact-gated bit-identical, so WHICH one runs is a
|
|
@@ -502,7 +570,7 @@
|
|
| 502 |
// benchmark. First run of a fresh build is warm-up-skewed; the race
|
| 503 |
// warms both before timing.
|
| 504 |
const dp4 = { backend: "webgpu", label: `${gpuName} (DP4A int8 dot Β· exact-gated vs units)`,
|
| 505 |
-
bgemm: bg, att, fgemm, mlp: mlpDp4 };
|
| 506 |
try {
|
| 507 |
const d0 = { m: 256, k: 32, n: 32, batch: 3, relu: false };
|
| 508 |
const rnd8 = (len) => { const a = new Int8Array(len); for (let i = 0; i < len; i++) a[i] = (Math.random() * 256 - 128) | 0; return a; };
|
|
@@ -618,6 +686,57 @@
|
|
| 618 |
return out;
|
| 619 |
}
|
| 620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
// fused batched dispatch: f32 out, epilogue done on-device (raw=true reads the
|
| 622 |
// verify variant's int32 accumulator instead)
|
| 623 |
async function runBgPass(device, pipeline, entries, m, n, batch, raw) {
|
|
|
|
| 363 |
// split-K f32 GEMM for the STE backward (self-tested vs JS float matmul)
|
| 364 |
const fPipes = { gemm: mkPipe(WGSL_FGEMM), reduce: mkPipe(WGSL_FREDUCE) };
|
| 365 |
let fgemm = (A, Bm, d) => gpuFgemm(device, fPipes, A, Bm, d);
|
| 366 |
+
let fgemm2 = (A, B1, d1, B2, d2) => gpuFgemm2(device, fPipes, A, B1, d1, B2, d2);
|
| 367 |
try {
|
| 368 |
const m0 = 7, k0 = 4500, n0 = 5; // k big enough to exercise split-K
|
| 369 |
const A = Float32Array.from({ length: m0 * k0 }, () => Math.random() - 0.5);
|
|
|
|
| 372 |
const ref = root.TrainCore.matmul(A, Bm, m0, k0, n0);
|
| 373 |
for (let i = 0; i < ref.length; i++)
|
| 374 |
if (Math.abs(hw[i] - ref[i]) > Math.abs(ref[i]) * 1e-3 + 1e-3) throw new Error("fgemm mismatch");
|
| 375 |
+
// The shared-operand pair must equal the two calls it replaces, EXACTLY:
|
| 376 |
+
// same buffer, same shader, same arithmetic, so bit-level equality is
|
| 377 |
+
// the honest bar (not a tolerance). Both index patterns of A are
|
| 378 |
+
// exercised β transA on one leg, plain on the other, the live shapes.
|
| 379 |
+
const kT = 300, mT = 9, nT = 6; // A is mT x kT, used both ways
|
| 380 |
+
const A2 = Float32Array.from({ length: mT * kT }, () => Math.random() - 0.5);
|
| 381 |
+
const Bt = Float32Array.from({ length: mT * nT }, () => Math.random() - 0.5);
|
| 382 |
+
const Bn = Float32Array.from({ length: kT * nT }, () => Math.random() - 0.5);
|
| 383 |
+
const dT = { m: kT, k: mT, n: nT, transA: true }, dN = { m: mT, k: kT, n: nT };
|
| 384 |
+
const [f1, f2] = await fgemm2(A2, Bt, dT, Bn, dN);
|
| 385 |
+
const [r1, r2] = [await fgemm(A2, Bt, dT), await fgemm(A2, Bn, dN)];
|
| 386 |
+
for (let i = 0; i < r1.length; i++) if (bitDiff(f1[i], r1[i])) throw new Error("fgemm2 transA leg differs");
|
| 387 |
+
for (let i = 0; i < r2.length; i++) if (bitDiff(f2[i], r2[i])) throw new Error("fgemm2 plain leg differs");
|
| 388 |
} catch (e) {
|
| 389 |
console.warn("split-K f32 GEMM failed verification β backward stays in JS:", e.message);
|
| 390 |
+
fgemm = null; fgemm2 = null;
|
| 391 |
}
|
| 392 |
|
| 393 |
// Shared exact gate for a bgemm implementation. Sweeps shapes (including
|
|
|
|
| 418 |
return null;
|
| 419 |
}
|
| 420 |
|
| 421 |
+
// Poison the pool with large-magnitude residue at the gate's own shapes,
|
| 422 |
+
// so a re-gate runs on RECYCLED (non-zero) buffers. See the dirty-buffer
|
| 423 |
+
// gate note below the MLP gate for why this matters.
|
| 424 |
+
async function poisonBgemm(bgFn) {
|
| 425 |
+
for (const d0 of [{ m: 5, k: 9, n: 6, batch: 3, relu: true },
|
| 426 |
+
{ m: 32, k: 64, n: 32, batch: 1, relu: false },
|
| 427 |
+
{ m: 7, k: 253, n: 5, batch: 2, relu: true },
|
| 428 |
+
{ m: 1, k: 4, n: 1, batch: 1, relu: false },
|
| 429 |
+
{ m: 17, k: 33, n: 9, batch: 1, relu: true }]) {
|
| 430 |
+
const Xq = new Int8Array(d0.batch * d0.m * d0.k).fill(127);
|
| 431 |
+
const Wq = new Int8Array(d0.batch * d0.k * d0.n).fill(127);
|
| 432 |
+
const rs = new Float32Array(d0.batch * d0.m).fill(1e4);
|
| 433 |
+
const cs = new Float32Array(d0.batch * d0.n).fill(1e4);
|
| 434 |
+
await bgFn(Xq, Wq, rs, cs, d0);
|
| 435 |
+
await bgFn(Xq, Wq, rs, cs, { ...d0, acc: true });
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
const gateBgemmDirty = async (bgFn) => { await poisonBgemm(bgFn); return gateBgemm(bgFn); };
|
| 439 |
+
|
| 440 |
const bgLut = (Xq, Wq, rs, cs, d) => gpuBgemmLUT(device, d.acc ? bgLutVPipe : bgLutPipe, lutBuf, Xq, Wq, rs, cs, d);
|
| 441 |
// the LUT bgemm is the fallback AND the oracle's shader twin β gate it too
|
| 442 |
+
const lutBad = (await gateBgemm(bgLut)) || (await gateBgemmDirty(bgLut));
|
| 443 |
if (lutBad) { console.warn("LUT bgemm shader failed verification β CPU mirrors only:", lutBad); return cpu; }
|
| 444 |
|
| 445 |
// B2B MLP chain gate (CUTLASS ex. 13+23): run the WHOLE chain β gemm1 +
|
| 446 |
// ReLU + on-GPU rowmax + on-GPU quantize + gemm2 β against the pure-JS
|
| 447 |
// mirror chain, exact `!==` on both h1 and the final output. Sweeps
|
| 448 |
// ragged shapes; h not a multiple of 4 exercises the pack-tail padding.
|
| 449 |
+
const MLP_SHAPES = [{ m: 6, k: 8, h: 12, n: 5 }, { m: 5, k: 16, h: 6, n: 3 },
|
| 450 |
+
{ m: 17, k: 33, h: 10, n: 9 }, { m: 32, k: 64, h: 128, n: 32 }];
|
| 451 |
+
async function gateMlp(mlpFn, sweepOnly) {
|
| 452 |
+
for (const d0 of MLP_SHAPES) {
|
| 453 |
const rnd = (len) => Float32Array.from({ length: len }, () => Math.random() * 2 - 1);
|
| 454 |
const Xf = rnd(d0.m * d0.k), W1 = rnd(d0.k * d0.h), W2 = rnd(d0.h * d0.n);
|
| 455 |
const hw = await root.Verified.vmlpBlock(Xf, W1, W2, d0, L, mlpFn, null);
|
|
|
|
| 460 |
for (let i = 0; i < ref.out.length; i++)
|
| 461 |
if (bitDiff(hw.out[i], ref.out[i])) return `out mismatch @${i} (${shape}): ${hw.out[i]} vs ${ref.out[i]}`;
|
| 462 |
}
|
| 463 |
+
if (sweepOnly) return null; // dirty re-gate: sweep is the point, skip the respec hunt
|
| 464 |
// DISCRIMINATING case: the sweep above passes vacuously if no value
|
| 465 |
// lands on a rounding boundary β the old round(x/scale) spec would
|
| 466 |
// pass it too. So hunt (in fast JS) for an input where the two specs
|
|
|
|
| 502 |
// test_b2b.js: 48M draws targeted at binade edges, 1.9M last-ulp
|
| 503 |
// fused-vs-stepped differences, ZERO floor-visible. The `+0.5` respec is
|
| 504 |
// contraction-immune by construction β `round(x/scale)` was not.
|
| 505 |
+
// ---- DIRTY-BUFFER GATE ----------------------------------------------
|
| 506 |
+
// A pooled buffer is NOT zero-initialized, so any kernel that assumes
|
| 507 |
+
// zeros (the rowmax atomicMax accumulator does) is right on step one and
|
| 508 |
+
// wrong on step two. That is a STATE bug: no single call is wrong, the
|
| 509 |
+
// sequence is β the family an oracle cannot reach.
|
| 510 |
+
//
|
| 511 |
+
// MEASURED, and it corrected the assumption that motivated this code:
|
| 512 |
+
// the existing sweep ALREADY catches it. Deleting the clearBuffer and
|
| 513 |
+
// re-running showed the plain gate failing at the SECOND shape, because
|
| 514 |
+
// the sweep's own shapes recycle each other's buffers (same power-of-2
|
| 515 |
+
// bucket) and an uncleared max only grows. So the suite was never
|
| 516 |
+
// blind β it had incidental dirty coverage nobody designed.
|
| 517 |
+
//
|
| 518 |
+
// Incidental is the problem. It relies on the sweep having >= 2 shapes,
|
| 519 |
+
// on them colliding in one bucket, and on the residue exceeding the real
|
| 520 |
+
// value. Change the shape list and the coverage silently evaporates.
|
| 521 |
+
// The poison below makes it deliberate: run first at 1e4 magnitude so
|
| 522 |
+
// the residue dominates ANY value the gate can produce, release it, then
|
| 523 |
+
// sweep again. Detection stops depending on ordering luck and covers the
|
| 524 |
+
// first shape too. Cheap by design β the re-gate skips the 800-trial
|
| 525 |
+
// respec hunt, which has nothing to do with buffer state.
|
| 526 |
+
async function poisonPool(mlpFn) {
|
| 527 |
+
// same shapes as the gate => same pool buckets => the gate's next
|
| 528 |
+
// acquisition is exactly one of these poisoned buffers (free list is LIFO)
|
| 529 |
+
const big = (len) => Float32Array.from({ length: len }, () => (Math.random() * 2 - 1) * 1e4);
|
| 530 |
+
for (const d0 of MLP_SHAPES)
|
| 531 |
+
await root.Verified.vmlpBlock(big(d0.m * d0.k), big(d0.k * d0.h), big(d0.h * d0.n), d0, L, mlpFn, null);
|
| 532 |
+
}
|
| 533 |
+
async function gateMlpDirty(mlpFn) {
|
| 534 |
+
await poisonPool(mlpFn);
|
| 535 |
+
return gateMlp(mlpFn, true); // sweep only, on recycled non-zero buffers
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
const lutMlpEnv = { dp4: false, gemm: bgLutPipe, rowmax: rowmaxPipe, quant: quantI32Pipe, lutBuf };
|
| 539 |
let mlpLut = (xq, w1q, w2q, xs, w1s, w2s, d) => gpuMlpChain(device, lutMlpEnv, xq, w1q, w2q, xs, w1s, w2s, d);
|
| 540 |
+
const mlpLutBad = (await gateMlp(mlpLut)) || (await gateMlpDirty(mlpLut));
|
| 541 |
if (mlpLutBad) { console.warn("B2B MLP chain (LUT) failed verification β MLP stays on the CPU mirror chain:", mlpLutBad); mlpLut = null; }
|
| 542 |
|
| 543 |
const viaLUT = { backend: "webgpu", label: `${gpuName} (LUT shader Β· exact-gated)`,
|
| 544 |
matmulInt8: (Xq, Wq, m, k, n) => gpuMatmulLUT(device, lutPipe, lutBuf, Xq, Wq, m, k, n),
|
| 545 |
+
bgemm: bgLut, att, fgemm, fgemm2, mlp: mlpLut };
|
| 546 |
|
| 547 |
// DP4A pipeline β only if the WGSL feature exists AND its batched kernel
|
| 548 |
// reproduces the verified units exactly across the shape sweep
|
|
|
|
| 550 |
return viaLUT;
|
| 551 |
const bgDp4Pipe = mkPipeL(WGSL_BG_DP4(false), bgDp4Layout), bgDp4VPipe = mkPipeL(WGSL_BG_DP4(true), bgDp4Layout);
|
| 552 |
const bg = (Xq, Wq, rs, cs, d) => gpuBgemmDP4(device, d.acc ? bgDp4VPipe : bgDp4Pipe, Xq, Wq, rs, cs, d);
|
| 553 |
+
const dp4Bad = (await gateBgemm(bg)) || (await gateBgemmDirty(bg));
|
| 554 |
if (dp4Bad) {
|
| 555 |
console.warn("batched DP4A disagreed with the verified units β using LUT bgemm:", dp4Bad);
|
| 556 |
return viaLUT;
|
| 557 |
}
|
| 558 |
const dp4MlpEnv = { dp4: true, gemm: bgDp4Pipe, rowmax: rowmaxPipe, quant: quantPackPipe };
|
| 559 |
let mlpDp4 = (xq, w1q, w2q, xs, w1s, w2s, d) => gpuMlpChain(device, dp4MlpEnv, xq, w1q, w2q, xs, w1s, w2s, d);
|
| 560 |
+
const mlpDp4Bad = (await gateMlp(mlpDp4)) || (await gateMlpDirty(mlpDp4));
|
| 561 |
if (mlpDp4Bad) { console.warn("B2B MLP chain (DP4A) failed verification β using the LUT chain:", mlpDp4Bad); mlpDp4 = mlpLut; }
|
| 562 |
|
| 563 |
// Both backends are exact-gated bit-identical, so WHICH one runs is a
|
|
|
|
| 570 |
// benchmark. First run of a fresh build is warm-up-skewed; the race
|
| 571 |
// warms both before timing.
|
| 572 |
const dp4 = { backend: "webgpu", label: `${gpuName} (DP4A int8 dot Β· exact-gated vs units)`,
|
| 573 |
+
bgemm: bg, att, fgemm, fgemm2, mlp: mlpDp4 };
|
| 574 |
try {
|
| 575 |
const d0 = { m: 256, k: 32, n: 32, batch: 3, relu: false };
|
| 576 |
const rnd8 = (len) => { const a = new Int8Array(len); for (let i = 0; i < len; i++) a[i] = (Math.random() * 256 - 128) | 0; return a; };
|
|
|
|
| 686 |
return out;
|
| 687 |
}
|
| 688 |
|
| 689 |
+
// SHARED-OPERAND fused f32 GEMM pair. The two embedding-gradient GEMMs both
|
| 690 |
+
// consume dlogits (BT x vocab). With the 16512-token vocab that operand is
|
| 691 |
+
// ~17 MB, and running them as two independent calls uploaded it TWICE and
|
| 692 |
+
// paid two submits and two map round trips β profiling put the pair at 55%
|
| 693 |
+
// of the whole step. Here A goes up ONCE, both GEMM+reduce chains ride one
|
| 694 |
+
// command encoder, one submit covers both, and the two readbacks are mapped
|
| 695 |
+
// concurrently. The shader reads A as either A[row*k+p] or A[p*m+row]
|
| 696 |
+
// (transA), so ONE flat buffer serves both index patterns β nothing about
|
| 697 |
+
// the arithmetic changes, which is why the gradients stay bit-identical.
|
| 698 |
+
async function gpuFgemm2(device, pipes, A, B1, d1, B2, d2) {
|
| 699 |
+
const SU = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
| 700 |
+
const UU = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
|
| 701 |
+
const bufA = up(device, A, SU); // the shared 17 MB operand: ONE upload
|
| 702 |
+
const enc = device.createCommandEncoder();
|
| 703 |
+
const legs = [];
|
| 704 |
+
for (const [Bm, d] of [[B1, d1], [B2, d2]]) {
|
| 705 |
+
const { m, k, n } = d, transA = d.transA ? 1 : 0;
|
| 706 |
+
const S = k > 4096 ? Math.min(16, Math.ceil(k / 2048)) : 1;
|
| 707 |
+
const bufB = up(device, Bm, SU);
|
| 708 |
+
const bufP = mk(device, S * m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 709 |
+
const bufD1 = up(device, new Uint32Array([m, k, n, transA | (S << 1)]), UU);
|
| 710 |
+
const bufO = mk(device, m * n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
| 711 |
+
const bufD2 = up(device, new Uint32Array([m * n, S, 0, 0]), UU);
|
| 712 |
+
let pass = enc.beginComputePass();
|
| 713 |
+
pass.setPipeline(pipes.gemm);
|
| 714 |
+
pass.setBindGroup(0, device.createBindGroup({ layout: pipes.gemm.getBindGroupLayout(0), entries: [
|
| 715 |
+
{ binding: 0, resource: { buffer: bufA } }, { binding: 1, resource: { buffer: bufB } },
|
| 716 |
+
{ binding: 2, resource: { buffer: bufP } }, { binding: 3, resource: { buffer: bufD1 } } ] }));
|
| 717 |
+
pass.dispatchWorkgroups(Math.ceil(m / 8), Math.ceil(n / 8), S); pass.end();
|
| 718 |
+
pass = enc.beginComputePass();
|
| 719 |
+
pass.setPipeline(pipes.reduce);
|
| 720 |
+
pass.setBindGroup(0, device.createBindGroup({ layout: pipes.reduce.getBindGroupLayout(0), entries: [
|
| 721 |
+
{ binding: 0, resource: { buffer: bufP } }, { binding: 1, resource: { buffer: bufO } },
|
| 722 |
+
{ binding: 2, resource: { buffer: bufD2 } } ] }));
|
| 723 |
+
pass.dispatchWorkgroups(Math.ceil(m * n / 64)); pass.end();
|
| 724 |
+
const bytes = m * n * 4;
|
| 725 |
+
const read = mk(device, bytes, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
|
| 726 |
+
enc.copyBufferToBuffer(bufO, 0, read, 0, bytes);
|
| 727 |
+
legs.push({ read, bytes, bufs: [bufB, bufP, bufD1, bufO, bufD2] });
|
| 728 |
+
}
|
| 729 |
+
device.queue.submit([enc.finish()]); // ONE submit for both GEMMs
|
| 730 |
+
await Promise.all(legs.map((l) => l.read.mapAsync(GPUMapMode.READ)));
|
| 731 |
+
const outs = legs.map((l) => {
|
| 732 |
+
const o = new Float32Array(l.read.getMappedRange(0, l.bytes).slice(0));
|
| 733 |
+
l.read.unmap();
|
| 734 |
+
return o;
|
| 735 |
+
});
|
| 736 |
+
release(device, [bufA, ...legs.flatMap((l) => [...l.bufs, l.read])]);
|
| 737 |
+
return outs;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
// fused batched dispatch: f32 out, epilogue done on-device (raw=true reads the
|
| 741 |
// verify variant's int32 accumulator instead)
|
| 742 |
async function runBgPass(device, pipeline, entries, m, n, batch, raw) {
|