Skip to content

Commit 7cc9074

Browse files
committed
docs(§TB1): TRIDAO-BATCHED-BACKWARD — MEASURED CUDA NO-GO (smem ceiling) + Metal GO (in-budget 4.99x) + z3 non-vacuous
CUDA gb10 sm_121 (prod b1 S4096 c64 G8 H112 P64 N64): batched HPC=1 = 1234ms = 0.715x vs v1-threaded 882ms (NO-GO); §27 single-tile 1215ms 0.726x; HPC>=2 fails to launch (66560 B dynamic smem, static+dynamic > 101376 optin ceiling). The head- amortization lever cannot fit; the only launchable config degenerates to the §27 twin. Self-consistency vs v1 worst 1.98e-5 (math correct, perf NO-GO). Metal local Apple (simdgroup, in-budget L=P=N=32): HPC=1 = 2.17ms vs scalar 10.82ms = 4.99x GO, full parity 6.03e-6. prod L=P=N=64 = SMEM-NO-GO (49664>32768 Apple pool). HPC=2 cross-head dchunk bug (6.62e-3) documented; HPC=1 ships. z3: 3 positives proved + 2 non-vacuity negatives fail correctly (gb10, z3 live).
1 parent afa42a7 commit 7cc9074

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

docs/TRIDAO-BATCHED-BACKWARD.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)