Quazim0t0 commited on
Commit
b5aa10b
Β·
verified Β·
1 Parent(s): 535c637

Upload web/public/webgpu.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
- async function gateMlp(mlpFn) {
417
- for (const d0 of [{ m: 6, k: 8, h: 12, n: 5 }, { m: 5, k: 16, h: 6, n: 3 },
418
- { m: 17, k: 33, h: 10, n: 9 }, { m: 32, k: 64, h: 128, n: 32 }]) {
 
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) {