|
| 1 | +# §P1 — Megatron/cppmega vs path_c BACKWARD: step-level profiler + code diff (gb10 sm_121) |
| 2 | + |
| 3 | +Date: 2026-06-05 |
| 4 | +Author: David Gornshtein (davidgornshtein@gmail.com) |
| 5 | +Repo: `/Volumes/external/sources/cppmega.mlx` |
| 6 | +Box: gb10 / DGX-Spark, NVIDIA GB10, sm_121 (consumer Blackwell; `mma.sync m16n8k16`, |
| 7 | +NO wgmma/tcgen05, ~99 KB per-threadgroup dyn-smem cap). |
| 8 | +TileLang dev root (path_c): `/home/dave/source/tilelang` on branch |
| 9 | +`merge/upstream-codegen-reorg`. |
| 10 | + |
| 11 | +## TL;DR — the "120x / fundamental" framing is REFUTED with measurement |
| 12 | + |
| 13 | +The user was right: "fundamental" was unproven. Here is the MEASURED answer. |
| 14 | + |
| 15 | +1. **The "120x on the same engine" (cppmega 3.707 ms vs path_c 447.8 ms) is |
| 16 | + APPLES-TO-ORANGES on THREE axes and is NOT real.** |
| 17 | + - The `3.70674 ms` is the cppmega TileLang **MIMO** `mamba_mimo_bwd_bwd` (the |
| 18 | + gradient/VJP kernel of the `mamba_mimo_bwd_combined` first-order backward) at |
| 19 | + cppmega's **productionish** shape `B=4,S=4096,H=32,G=1,N=64,P=128,R=4,chunk=16`, |
| 20 | + measured on **H200** (Hopper, wgmma tensor cores). Source: cppmega |
| 21 | + `docs/status/mamba3_cuda_bwd_bwd_10wave_summary_2026_04_30.md:72` and |
| 22 | + `mamba3_bwd_bwd_owner_rewrite_wave1_2026_04_29.md:61` (baseline |
| 23 | + `bwd_fwd 1.8718 + bwd_bwd 3.7084 = chain 5.5628 ms`). |
| 24 | + - The `447.8 ms` was path_c's B2→B1→B0 VJP chain at path_c **prod** |
| 25 | + `bs1,S=4096,c=64,G=8,H=112,P=64,N=64` — but at a RE-GRIDDED / reduced-state |
| 26 | + substitution (§17), because the full-N=64 scan tile does not fit the gb10 smem |
| 27 | + cap (see below). The HONEST fresh full-config number is ~1300 ms (this doc). |
| 28 | + - Differences: (a) different HARDWARE (H200 wgmma vs gb10 sm_121 mma.sync); |
| 29 | + (b) different SHAPE (H32/G1/P128/chunk16 vs H112/G8/P64/chunk64 — ~3.5x heads, |
| 30 | + 8x groups, 4x chunk); (c) the path_c number was a reduced-state substitution, |
| 31 | + not the full kernel. |
| 32 | + |
| 33 | +2. **cppmega's shipped MIMO TileLang backward DOES NOT COMPILE on the current gb10 |
| 34 | + dev TileLang** (`merge/upstream-codegen-reorg`): SEGFAULT in the tirx |
| 35 | + `ExprFunctor/AccessPath` visitor at BOTH productionish and representative shapes |
| 36 | + (see "cppmega backward on gb10" below). So a like-for-like same-box cppmega-MIMO |
| 37 | + number cannot be produced on this branch; its authoritative numbers are H200. |
| 38 | + |
| 39 | +3. **What Megatron/cppmega does CONCRETELY faster (the real lever, MEASURED):** |
| 40 | + cppmega's full-model training step does NOT use the TileLang MIMO kernels for the |
| 41 | + hot mamba path at all — it runs the **fused Triton SSD** `mamba_chunk_scan_combined` |
| 42 | + (one smem-resident kernel: cb, dA-cumsum, chunk-states, scan, combine), measured on |
| 43 | + gb10 at **3.11 ms fwd / ~10 ms bwd**. path_c splits the SAME SSD math into separate |
| 44 | + gridded kernels F0/F1/F2 (fwd) and B2/B1/B0 (bwd), each round-tripping the chunk |
| 45 | + tensors through global memory, and **F0 precompute ALONE = 16.37 ms ≥ 5.7x cppmega's |
| 46 | + ENTIRE fused fwd**. The lever is FUSION + smem-residency of the chunk state, not a |
| 47 | + different algorithm and not host/launch staging. |
| 48 | + |
| 49 | +## Config matched / reconciliation |
| 50 | + |
| 51 | +| stack | what it is | shape | hardware | source | |
| 52 | +| --- | --- | --- | --- | --- | |
| 53 | +| cppmega MIMO bwd (`mamba_mimo_bwd_bwd`) | TileLang VJP kernel | B4 S4096 H32 G1 N64 P128 R4 c16 | **H200** | cppmega status docs | |
| 54 | +| cppmega MIMO `bwd_combined` chain | `bwd_fwd` + `bwd_bwd` (first-order) | same | H200 | 1.87 + 3.71 = **5.56 ms** | |
| 55 | +| cppmega Triton SSD (in the real step) | `mamba_chunk_scan_combined` fused | path_c prod tile S4096 c64 G8 H112 P64 N64 | **gb10** | MAMBA3-PATHC-VS-CPPMEGA.md: **3.11 ms fwd / ~10 ms bwd** | |
| 56 | +| path_c B2/B1/B0 VJP chain | 3 gridded TileLang kernels | bs1 S4096 c64 G8 H112 P64 N64 | **gb10** | THIS doc: **~1300 ms** | |
| 57 | + |
| 58 | +The only TRUE same-box (gb10), same-config, same-SSD-math comparison available is the |
| 59 | +last two rows: **cppmega fused Triton fwd 3.11 ms vs path_c F0 alone 16.37 ms (F2 won't |
| 60 | +even launch).** That is the real, honest gap. |
| 61 | + |
| 62 | +## MEASURED — path_c prod backward chain (gb10, fresh, full config) |
| 63 | + |
| 64 | +`scratch/probe_chunked_backward_cuda_gb10.py --prod`, cfg |
| 65 | +`bs1,S=4096,c=64,G=8,H=112,P=64,N=64,nchunks=64`, CUDA-event medians: |
| 66 | + |
| 67 | +| prim | what it computes | median ms/call | |
| 68 | +| --- | --- | ---: | |
| 69 | +| **B2** `chunk_scan_combine_bwd` | the VJP scan-combine (dC/dx/dz/dchunk_states/dinp_diag/dA_y/dD) | **1026.2** | |
| 70 | +| **B1** `inter_chunk_recur_bwd` | cross-chunk reverse adjoint (O(S/c)) | **5.30** | |
| 71 | +| **B0** `chunk_precompute_bwd` | dinp split (dx/dB/ddt + dstates-coupled) | **269.2** | |
| 72 | +| **chain total** | B2→B1→B0 | **~1300.8** | |
| 73 | + |
| 74 | +8-grad parity PASS (gate 1e-3, all elements): `dz=1.73e-04 dx=8.10e-04 dC=5.03e-05 |
| 75 | +dB=1.09e-05 dlog_decay=6.68e-04 ddt=1.50e-04 dh0=1.84e-04 dD=2.19e-05`, WORST=8.10e-04. |
| 76 | + |
| 77 | +### Isolated tilelang-only B2 (no reference, no resets) — gb10 CUDA events |
| 78 | + |
| 79 | +To remove the probe's reference-VJP and the 4.7 GB `dinp_diag.zero_()` per call from the |
| 80 | +measurement, the B2 prim was timed in isolation feeding only the kernel: |
| 81 | +**`B2_ISOLATED_KERNEL_ONLY median_ms = 827.3`** (`/tmp/b2_isolated.py`, 10-iter median, |
| 82 | +min 826.6 / max 828.7). So the genuine tilelang B2 device time is **~827 ms**; the |
| 83 | +probe's 1026 ms includes the per-call zero-fills of the giant grad scratch tensors. |
| 84 | + |
| 85 | +## gap LOCALIZED — it is B2, and B2 is COMPUTE-bound serial reduction |
| 86 | + |
| 87 | +The backward gap is concentrated in **B2 (~827 ms isolated / ~1026 ms in-chain)**; B0 is |
| 88 | +secondary (~266 ms) and B1 is negligible (~5 ms). The WHY, from the code: |
| 89 | + |
| 90 | +- path_c's **default** prod B2 prim `chunk_scan_combine_bwd_cuda_prim` |
| 91 | + (`mamba3_chunked_backward_core.py:413`, the one prod actually runs with |
| 92 | + `CPPMEGA_PATH_C_B2_V2` unset) computes every backward contraction as **per-thread |
| 93 | + `T.serial` scalar reductions** over the lower-triangular `(s,l)` chunk pairs and over |
| 94 | + `headdim`/`dstate` — `grep`: 0 `T.gemm` in its body, many `T.serial`/`T.Parallel` |
| 95 | + scalar loops. It is `T.Kernel(grid)`-parallel across chunks/heads but each thread does |
| 96 | + scalar MACs. This is compute-bound: the isolated 827 ms is pure GPU device time on a |
| 97 | + single long-running grid (§18 already found these B2 kernels occupancy-SATURATED at |
| 98 | + bs1, v2 dstate-split 0.997x → not launch-bound, not occupancy-starved). |
| 99 | +- cppmega's `mamba_mimo_bwd_bwd` (`state-spaces-mamba/.../mamba3_mimo_bwd.py`) does the |
| 100 | + SAME contractions as **15 `T.gemm` tensor-core matmuls** (dk, dq, dPsiV, dqk, dstates; |
| 101 | + lines 811/822/846/905/930/952/980/1026/1050/1148 …) with the chunk state RESIDENT in |
| 102 | + shared memory, grid `T.Kernel(H, B, threads=256)` — ONE block per (head, batch) that |
| 103 | + loops over `nchunks` INTERNALLY with no global round-trip per chunk. |
| 104 | + |
| 105 | +### Why path_c can't just adopt cppmega's tensor-core GEMM B2 at prod (MEASURED NO-GO) |
| 106 | + |
| 107 | +path_c already HAS a tensor-core GEMM B2 prim (`chunk_scan_combine_bwd_cuda_prim_gemm`, |
| 108 | +the §27 four-GEMM body). At prod dims, single-process A/B on gb10: |
| 109 | + |
| 110 | +``` |
| 111 | +[B2-GEMM-AB] MEASURED v1(serial)=1020.3 ms gemm(tensor-core)=1352.9 ms |
| 112 | + speedup=0.754x NO-GO (math-equivalent, max|abs| dC 6.2e-5 / dA_y 3.2e-5) |
| 113 | +``` |
| 114 | + |
| 115 | +The GEMM B2 is **0.754x — SLOWER** than the serial B2 at path_c's prod tile. Reason |
| 116 | +(§MF1 / §27): at `c=64,P=64,N=64` the per-CTA smem to stage the GEMM operands AND keep |
| 117 | +the chunk state resident blows the gb10 99 KB cap (the resident `dinp_diag[L,P·N]` tile |
| 118 | +alone = 1 MB), so the GEMM variant cannot fill the 4-warp `mma.sync` partition AND fit |
| 119 | +smem simultaneously → it pays the staging/sync cost without the tensor-core throughput. |
| 120 | +cppmega avoids this because its productionish uses **chunk=16** (4x smaller chunk → the |
| 121 | +resident chunk tile fits smem) on **H200** (wgmma + larger smem). So "port cppmega's |
| 122 | +gridded GEMM backward" does NOT close the gap at path_c's gb10 prod tile — MEASURED. |
| 123 | + |
| 124 | +## cppmega backward on gb10 — SEGFAULT on the current dev TileLang (honest NO-RUN) |
| 125 | + |
| 126 | +`mamba_mimo_bwd_combined` (the shipped cppmega MIMO first-order backward = |
| 127 | +`mamba_mimo_bwd_fwd` recompute + `mamba_mimo_bwd_bwd` VJP) was driven on gb10 at BOTH |
| 128 | +productionish (B4 S4096 H32 G1 N64 P128 R4 c16) and representative (B2 S1024 H16 …): |
| 129 | + |
| 130 | +``` |
| 131 | +!!!!!!! Segfault encountered !!!!!!! |
| 132 | + in tvm::tirx::ExprFunctor<void (PrimExpr const&, AccessPath)>::VisitExpr(...) |
| 133 | + in tvm::tirx::TIRVisitorWithPath::VisitStmt_(AttrStmtNode const*, AccessPath) |
| 134 | +(exit 139, before the kernel even runs) |
| 135 | +``` |
| 136 | + |
| 137 | +The segfault is in the **tirx AccessPath visitor** of the `merge/upstream-codegen-reorg` |
| 138 | +dev TileLang that path_c uses — a COMPILER regression on this branch, not a kernel bug. |
| 139 | +cppmega's MIMO backward was authored/measured against an older stable TileLang on H200; |
| 140 | +the stable build on gb10 (`/home/dave/tilelang-build`) is not fully wired into the venv |
| 141 | +(missing `libz3.so.4.16` / tvm_ffi bindings) so a same-box cppmega-MIMO number is not |
| 142 | +producible on this branch. Recorded as MEASURED NO-RUN (RULE #1: surfaced, not faked). |
| 143 | + |
| 144 | +## GPU-busy% / launch-bound vs compute-bound (MEASURED, with an honest caveat) |
| 145 | + |
| 146 | +- **path_c B2 is COMPUTE-bound.** The isolated tilelang-only device time is **827 ms** |
| 147 | + (CUDA events, on-stream) for a single grid — there is no host gap inside it; §18 |
| 148 | + measured it occupancy-saturated (dstate-split 0.997x) and batch-invariant at bs1. It |
| 149 | + is slow because it does the contractions as scalar serial MACs, not because of |
| 150 | + launch/occupancy. |
| 151 | +- **nsys CAVEAT (honest):** `nsys profile --trace=cuda` on gb10 sm_121 does NOT capture |
| 152 | + the TileLang `main_kernel` launches (the TVM runtime launch path is not symbolized by |
| 153 | + nsys's CUDA injection on this box) — the `cuda_gpu_kern_sum` for the isolated B2 run is |
| 154 | + empty of tilelang kernels (`"does not contain CUDA kernel data"`), and the full-probe |
| 155 | + trace shows only the torch reference-VJP `at::native` elementwise/`gemvx` kernels + |
| 156 | + the 4.7 GB `dinp_diag` zero-fills. So GPU-busy% via nsys is unavailable for the |
| 157 | + tilelang kernels on this branch; the compute-bound conclusion rests on the on-stream |
| 158 | + CUDA-event device time (827 ms, no host bubble) + the §18 occupancy receipts. |
| 159 | +- **At the STEP level the cppmega advantage is ALSO launch-amortization:** cppmega runs |
| 160 | + the full step under CUDA-graph, fully device-resident, where mamba+m2rnn are only |
| 161 | + ~24% of the iter (nam56r nsys: MIMO 17.5% + M2RNN 6.2%) at ~3692 tok/s / 4437 ms-iter |
| 162 | + on gb10 — but that is the ENGINE wrapper, secondary to the per-kernel fusion gap above. |
| 163 | + |
| 164 | +## Concrete WHAT Megatron/cppmega does differently (NOT fundamental) |
| 165 | + |
| 166 | +The SAME Mamba3/SSD math is faster in cppmega because of CONCRETE engineering choices, |
| 167 | +each reproducible: |
| 168 | + |
| 169 | +1. **Fused, smem-resident SSD kernel** (the dominant lever): cppmega's hot path is ONE |
| 170 | + fused Triton kernel (`mamba_chunk_scan_combined`, 3.11 ms fwd gb10) keeping the chunk |
| 171 | + state in shared memory; path_c splits it into F0/F1/F2 + B2/B1/B0 gridded kernels with |
| 172 | + global round-trips, and F0 ALONE is 16.37 ms (≥5.7x). FUSION, not algorithm. |
| 173 | +2. **Tensor-core GEMM contractions** in the VJP (cppmega MIMO bwd_bwd: 15 `T.gemm`) |
| 174 | + vs path_c's default serial scalar reductions — BUT this only pays off when the chunk |
| 175 | + tile fits smem (cppmega chunk=16 / H200), which it does NOT at path_c's gb10 prod tile |
| 176 | + (chunk=64, GEMM B2 = 0.754x SLOWER, MEASURED). So this lever is HW/shape-gated, not |
| 177 | + free. |
| 178 | +3. **Smaller chunk (16 vs 64)** so the resident chunk state fits the smem cap and the |
| 179 | + tensor-core partition stays full — the structural reason path_c's GEMM/mono B2 hit the |
| 180 | + 99 KB wall at chunk=64,N=P=64 (§MF1). |
| 181 | +4. **CUDA-graph + device-resident step** amortizing launch across the ~24%-mamba iter |
| 182 | + (engine wrapper, secondary). |
| 183 | + |
| 184 | +None of these is "fundamental." The honest path forward for path_c on gb10 is (a) FUSE |
| 185 | +F0/F1/F2 and B2/B1/B0 into smem-resident kernels and (b) drop chunk to 16-class so a |
| 186 | +tensor-core GEMM VJP fits the 99 KB cap — exactly what cppmega/Megatron does. The |
| 187 | +already-tried mono-fusion at chunk=64 is a measured smem NO-GO (§MF1); the lever is the |
| 188 | +chunk-size + fusion REDESIGN, not a one-kernel port. |
| 189 | + |
| 190 | +## Files / commands (all gb10, single-owner mutex) |
| 191 | + |
| 192 | +- path_c prod backward chain + per-grad gate: |
| 193 | + `scratch/probe_chunked_backward_cuda_gb10.py --prod` |
| 194 | +- B2 serial-vs-GEMM A/B at prod: `... --prod --b2-gemm-ab` |
| 195 | +- isolated tilelang-only B2 timer: `/tmp/b2_isolated.py` (gb10) |
| 196 | +- cppmega MIMO backward driver (SEGFAULTs on this branch): `/tmp/cppmega_bwd_bench.py` |
| 197 | + + `state-spaces-mamba/mamba_ssm/ops/tilelang/mamba3/mamba3_mimo_bwd.py` |
| 198 | + (`mamba_mimo_bwd_combined` / `mamba_mimo_bwd_fwd` / `mamba_mimo_bwd_bwd`) |
| 199 | +- cppmega Triton SSD fused (the real-step kernel): |
| 200 | + `mamba_ssm.ops.triton.ssd_combined.mamba_chunk_scan_combined` |
| 201 | +- per-region same-box compare (already MEASURED): `docs/MAMBA3-PATHC-VS-CPPMEGA.md`, |
| 202 | + `scratch/mamba3_m2rnn_compare.py --prod` |
| 203 | +- path_c B2 default serial prim: `cppmega_mlx/nn/_tilelang/mamba3_chunked_backward_core.py:413` |
| 204 | + (`chunk_scan_combine_bwd_cuda_prim`); GEMM prim same file |
| 205 | + (`chunk_scan_combine_bwd_cuda_prim_gemm`). |
0 commit comments