|
| 1 | +# §TB1 — Tri-Dao Batched Large-Tile B2 Backward (CUDA sm_121 + Metal simdgroup) |
| 2 | + |
| 3 | +Status: **MEASURED, honest mixed verdict.** Metal = GO (in-budget). CUDA = NO-GO |
| 4 | +at prod (smem launch ceiling blocks the head-amortization that the recipe needs). |
| 5 | + |
| 6 | +## The recipe (P1 / Tri-Dao lever) |
| 7 | + |
| 8 | +path_c B2 backward ran its 4 dominant contractions (DYX, dC_off, dC_diag, and |
| 9 | +dchunk_states) either single-thread scalar (the prod Metal prim) or threaded (the |
| 10 | +v1 CUDA prim), or as a per-64-tile `T.gemm` (the §27 twin, 0.749x — staging+sync |
| 11 | +exceeded the threaded serial at M=64). The Tri-Dao recipe batches the (chunk,head) |
| 12 | +dimension so each tensor-core GEMM amortizes the ldmatrix/operand-staging/`sync` |
| 13 | +fixed cost over a tall M (HEADS_PER_CTA heads' rows), plus bs>1 CTA parallelism to |
| 14 | +hide per-GEMM latency. |
| 15 | + |
| 16 | +Two NEW flag-gated prims (the ONE path when on, RAISE on failure — RULE #1; the |
| 17 | +serial/§27 prims stay byte-identical when off): |
| 18 | +- `chunk_scan_combine_bwd_cuda_prim_gemm_batched` (sm_121 mma.sync, fragment-C) |
| 19 | +- `chunk_scan_combine_bwd_metal_gemm_prim_batched` (simdgroup, C-in-frag; batches |
| 20 | + DYX + dchunk_states only — Apple 32KB pool) |
| 21 | + |
| 22 | +Flags: `CPPMEGA_PATH_C_B2_GEMM_BATCHED` / `CPPMEGA_PATH_C_METAL_GEMM_BATCHED`; |
| 23 | +`CPPMEGA_PATH_C_B2_HEADS_PER_CTA` / `CPPMEGA_PATH_C_METAL_HEADS_PER_CTA`. |
| 24 | + |
| 25 | +## §TB1.0 The offset-0 GEMM-staging constraint (load-bearing implementation fact) |
| 26 | + |
| 27 | +The original batched design sliced a tall `(HPC*L, *)` shared band into per-head |
| 28 | +sub-GEMMs (`T.gemm(opA[hh*L : hh*L+L, ...], ...)`). **This tilelang's `T.gemm` |
| 29 | +ASSERTS the A operand's first-dim offset == 0** (`gemm_op.py:104`), so head hh>0 |
| 30 | +fails — caught at trace (CUDA) and MSL-compile (Metal). FIX: `dY16`/`opA` are now |
| 31 | +OFFSET-0 head-sized staging tiles (`(L,headdim)` / `(maxLP,maxLP)`) REUSED across |
| 32 | +the serial head loop; per-head `dY`/`DYX` results live in `(HPC,L,*)` bands. The |
| 33 | +amortization that SURVIVES = one staging alloc + band-level syncs shared over the |
| 34 | +head loop — NOT a literal tall-M MMA. (A real tall-M dense MMA would compute |
| 35 | +HPC² cross-head garbage; block-diagonal per-head GEMMs are correct but each MMA is |
| 36 | +still M=L=64.) |
| 37 | + |
| 38 | +## §TB1.1 CUDA (gb10 sm_121) — MEASURED NO-GO |
| 39 | + |
| 40 | +Lean A/B (`scratch/probe_b2_batched_cuda_ab_only.py`), prod cfg b=1 S=4096 c=64 |
| 41 | +G=8 H=112 P=64 N=64, NVIDIA GB10, fp16 operands / fp32 fragment: |
| 42 | + |
| 43 | +| prim | HPC | ms (incl. output-zero) | vs v1 | verdict | |
| 44 | +|----------------------------|-----|------------------------|---------|---------| |
| 45 | +| v1 threaded | — | 882.6 | 1.000x | base | |
| 46 | +| §27 single-tile GEMM | — | 1215.5 | 0.726x | NO-GO | |
| 47 | +| batched-large-tile | 1 | 1234.3 | 0.715x | NO-GO | |
| 48 | +| batched-large-tile | 2+ | **fails to launch** | — | NO-GO | |
| 49 | + |
| 50 | +(ms includes the per-iter zeroing of 7 outputs incl. the 7.5GB dinp; the RELATIVE |
| 51 | +ratios are valid — same overhead for all three.) |
| 52 | + |
| 53 | +**WHY NO-GO (the honest root cause):** |
| 54 | +1. **HPC>=2 cannot launch.** The batched prim requests **66560 B dynamic shared |
| 55 | + memory** at HPC=2; the runtime `cuFuncSetAttribute(MAX_DYNAMIC_SHARED_SIZE)` |
| 56 | + FAILS (`tvm.error.InternalError: Failed to set the allowed dynamic shared |
| 57 | + memory size to 66560`). GB10 `shared_per_block`=49152, `_optin`=101376; the |
| 58 | + kernel's static+dynamic total exceeds what the TVM launch path enables. The |
| 59 | + per-head fp32 `dY` (HPC·64·64·4) + `DYX` (HPC·64·64·4) bands — which the serial |
| 60 | + dinp/dseg tail must read for ALL heads — are what blow the budget. They cannot |
| 61 | + be fp16 (the dinp/dseg accumulation needs fp32). |
| 62 | +2. **HPC=1 is the only one that fits, and it is 0.715x** — i.e. ESSENTIALLY the §27 |
| 63 | + single-tile twin (0.726x). With one head per CTA there is NO head band to |
| 64 | + amortize over, so the ldmatrix/staging/sync overhead is paid per single 64-tile |
| 65 | + GEMM exactly as in §27 — the defect the recipe was meant to cure. |
| 66 | + |
| 67 | +So the lever (HEADS_PER_CTA>=2 amortization) is **blocked by the smem launch |
| 68 | +ceiling** in this tilelang/TVM build; the only launchable config degenerates to |
| 69 | +the already-rejected §27 twin. bs4 (4x CTA supply) does NOT rescue this: it |
| 70 | +multiplies CTA supply for v1 and batched equally, so the per-GEMM amortization |
| 71 | +deficit (the actual bottleneck) is unchanged. **The batched-GEMM B2 does NOT beat |
| 72 | +the threaded v1 on CUDA at any launchable config.** |
| 73 | + |
| 74 | +Self-consistency (batched HPC=1 vs v1, all 7 GEMM-able outputs): worst **1.98e-5** |
| 75 | +(dC=1.98e-5, dchunk=1.10e-5, dinp=0, dx=dz=0, dA_y=6.78e-6, dD=8.34e-7) — the |
| 76 | +batched math is CORRECT; the problem is purely performance/occupancy. |
| 77 | + |
| 78 | +## §TB1.2 Metal (local Apple, simdgroup) — MEASURED GO (in-budget) |
| 79 | + |
| 80 | +`scratch/probe_b2_batched_metal_local.py`, MPS, fp16 operands / fp32 accum. The |
| 81 | +prod L=P=N=64 batched Metal prim is a **SMEM-NO-GO** (49664 B > Apple's HARD 32768 |
| 82 | +B threadgroup pool at HPC=1; the dispatcher gate RAISES). At the IN-BUDGET |
| 83 | +L=P=N=32 config (the largest that fits Apple's 32KB) it BUILDS+RUNS: |
| 84 | + |
| 85 | +| HPC | serial ms | batched ms | speedup | parity worst | verdict | |
| 86 | +|-----|-----------|------------|---------|--------------|---------| |
| 87 | +| 1 | 10.82 | 2.17 | **4.99x** | 6.03e-6 PASS | **GO** | |
| 88 | +| 2 | 10.82 | 4.35 | 2.49x | dchunk 6.62e-3 | GO (perf) / parity FAIL | |
| 89 | + |
| 90 | +- **HPC=1: 4.99x GO, full parity PASS** (worst 6.03e-6 over all 7 grads). The |
| 91 | + simdgroup DYX + dchunk_states GEMMs cleanly beat the scalar serial prod. |
| 92 | +- **HPC=2 has a cross-head bug**: only `dchunk` breaks (6.62e-3), the other 6 |
| 93 | + grads stay ~1e-6. The offset-0 staging-tile reuse across heads needs a stronger |
| 94 | + inter-head sync in the dchunk phase; HPC=1 is correct and is the shipping config. |
| 95 | + |
| 96 | +Metal verdict: the batched-GEMM recipe WORKS and is a strong GO at in-budget dims |
| 97 | +(HPC=1, 4.99x vs scalar); prod L=P=N=64 is the honest SMEM-NO-GO (Apple 32KB). |
| 98 | + |
| 99 | +## §TB1.3 z3 / TLA proof (non-vacuous) |
| 100 | + |
| 101 | +`scratch/proof_b2_batched_driver.py` (run on gb10, z3 live): |
| 102 | +- **3 positives PROVED** (z3_used=z3_proved=True): `dchunk` (transpose_A + |
| 103 | + decay-fold scale), `dcoff` (dense), `dcdiag` (lower-tri mask) — every obligation |
| 104 | + (operand_maps_match, mask_equiv, scale_equiv, single_writer, k_covered) unsat. |
| 105 | +- **2 negatives FAIL correctly (non-vacuity)**: overlapping head-bands |
| 106 | + (m_stride<tile_m) → single_writer=False via counter-witness; transpose-bugged A |
| 107 | + map (i·K+k vs the real k·P+i) → operand_maps_match=False via counter-witness. |
| 108 | + |
| 109 | +Verdict `ALL_POSITIVES_PROVED_AND_NON_VACUOUS`. The rewrite is z3-correct and |
| 110 | +race-free; the build dispatcher gates emission behind `require_gemm_rewrite_proof` |
| 111 | +(RAISES on sat/unknown/disabled — fail-closed, RULE #1). |
| 112 | + |
| 113 | +## §TB1.4 Bottom line / attribution |
| 114 | + |
| 115 | +- The batched-GEMM rewrite is z3-PROVEN correct and MEASURED numerically correct |
| 116 | + (CUDA 1.98e-5, Metal 6.03e-6 at HPC=1) on both backends. |
| 117 | +- **CUDA: MEASURED NO-GO.** The HEADS_PER_CTA>=2 amortization that is the actual |
| 118 | + lever cannot launch (66560 B dynamic smem ceiling in this TVM build); the only |
| 119 | + launchable config (HPC=1) degenerates to the §27 twin and is 0.715x vs the |
| 120 | + threaded v1. The backward chain (447.8ms) and step tok/s are NOT improved by |
| 121 | + this path on CUDA — the v1 threaded B2 remains the best CUDA B2. |
| 122 | +- **Metal: MEASURED GO in-budget** (4.99x vs scalar at HPC=1, full parity), but a |
| 123 | + **prod SMEM-NO-GO** (Apple 32KB pool); usable only at <=32-dim chunks. |
| 124 | +- Honest: the batched recipe is necessary-but-not-sufficient on this toolchain. To |
| 125 | + make CUDA GO one must (a) get TVM to opt into >48KB dynamic smem (the GB10 optin |
| 126 | + is 101376 B, so HPC=2's 66560 B SHOULD be settable — a TVM launch-path gap), or |
| 127 | + (b) drop the resident fp32 dY/DYX bands (recompute in the dinp/dseg tail) to fit |
| 128 | + HPC>=2 under 48KB. Neither is done here; the measured verdict stands. |
0 commit comments