|
| 1 | +# Tri-Dao mamba_ssm bwd Triton kernels through tilelang -> tvm on gb10 (STEP 3 route+measure) |
| 2 | + |
| 3 | +Date: 2026-06-06. Host: NVIDIA GB10 (sm_121), CUDA 13.2, driver 595.71.05. |
| 4 | +venv: `/home/dave/cppmega-venv` loading tilelang+tvm from SOURCE `/home/dave/source/tilelang` |
| 5 | +(commit `b5e4fed3`). triton 3.7.0, mamba_ssm 2.2.x. |
| 6 | + |
| 7 | +## Honest GO / NO-GO |
| 8 | + |
| 9 | +PARTIAL GO on COMPILE; **NO-GO on PARITY** (output is single-tile truncated — see bug below). |
| 10 | + |
| 11 | +- 3/7 group-A bwd kernels COMPILE END-TO-END through OUR frontend |
| 12 | + (`from_ttir`/`compile_ttir`) -> tilelang lower -> **TVM CUDA codegen** to a real |
| 13 | + `__global__` tensor-core kernel. NOT a stub, NOT native-triton fallback. |
| 14 | +- The routed `_chunk_scan_bwd_dstates` kernel **LAUNCHES on gb10** (executes, returns). |
| 15 | +- BUT the routed output is WRONG: only one program tile (4096 = hdim*dstate = 64*64) of |
| 16 | + the full `dprev_states` tensor is written. Parity vs native triton FAILS. Root cause is |
| 17 | + a real frontend output-buffer size-inference defect (below). Per RULE#1 this is reported |
| 18 | + loudly, not papered over. |
| 19 | + |
| 20 | +## Evidence the lowered PrimFunc is NON-EMPTY (real tt.dot body) |
| 21 | + |
| 22 | +`from_ttir(_chunk_scan_bwd_dstates, _allow_text_ttir=True, target=cuda)`: |
| 23 | +- PrimFunc length: **29448 chars**; `T.evaluate(0)` count: **0**; has `T.gemm(...)`: YES; has loops: YES. |
| 24 | +- Real body contains: `T.gemm(T.region(dot_a_shared...), T.region(tile_load_305...), T.region(dot_c_frag...))`, |
| 25 | + masked `tile_load_*[i]=T.if_then_else(mask, arg[idx], 0.0)`, `T.exp(...)` for dA_cumsum, |
| 26 | + full int64 index arithmetic with overflow guards. |
| 27 | + |
| 28 | +This comes from the NATIVE-PARSE provider (`triton_native_parse.py`, Provider 0), which uses |
| 29 | +the already-installed `triton._C.libtriton.ir.parse_mlir_module` to validate + canonically |
| 30 | +re-print the TTIR and adapt it for the existing OP_TABLE walker. NO C++ shim, NO mlir.ir/IREE |
| 31 | +build needed (the C++ PtrAnalysis shim is unavailable in-process because libtriton's static LLVM |
| 32 | +is already loaded; the frontend warns and uses the MVP scalar load/store path). |
| 33 | + |
| 34 | +## Evidence of REAL tilelang->tvm CUDA codegen (real HMMA from OUR codegen) |
| 35 | + |
| 36 | +`compile_ttir(...).get_kernel_source()` for `_chunk_scan_bwd_dstates_kernel` (33586 chars): |
| 37 | +``` |
| 38 | +#include <tl_templates/cuda/instruction/mma.h> |
| 39 | +... |
| 40 | +tl::mma_sync<tl::DataType::kTensorFloat32, tl::DataType::kTensorFloat32, |
| 41 | + tl::DataType::kFloat32, 16, 8, 8, false, true>( |
| 42 | + reinterpret_cast<float*>(dot_c_frag_308 + ...), |
| 43 | + reinterpret_cast<const uint32_t*>(A_local + ...), |
| 44 | + reinterpret_cast<const uint32_t*>(B_local + ...)); |
| 45 | +``` |
| 46 | +`tl::mma_sync<...kTensorFloat32...16,8,8...>` = m16n8k8 TF32 HMMA emitted by OUR tilelang |
| 47 | +CUDA template (`tl_templates/cuda/instruction/mma.h`), driven by TVM codegen. This is OUR |
| 48 | +codegen, NOT the triton runtime, NOT a stub. |
| 49 | + |
| 50 | +3/7 compile to this form: each `mma_sync`x2, `__global__`x2: |
| 51 | +- `_chunk_scan_bwd_dstates` (33586 chars), `_chunk_scan_bwd_dc` (34128), `_chunk_scan_bwd_dcb` (47676). |
| 52 | + |
| 53 | +## N / 7 status (group-A first) |
| 54 | + |
| 55 | +| kernel | TTIR capture | tilelang->tvm CUDA (real MMA) | launches on gb10 | correct output | |
| 56 | +|---|---|---|---|---| |
| 57 | +| _chunk_scan_bwd_dstates (A) | YES | YES (mma=2) | YES | NO (1-tile truncation) | |
| 58 | +| _chunk_scan_bwd_dc (A) | YES | YES (mma=2) | not run | NO (same bug) | |
| 59 | +| _chunk_scan_bwd_dcb (A) | YES | YES (mma=2) | not run | NO (same bug) | |
| 60 | +| _chunk_state_bwd_db (A) | YES | FAIL: `variables (arg4,) are used but not passed in` (tilelang var-binding) | - | - | |
| 61 | +| _chunk_scan_bwd_dx (B) | YES | FAIL: `m_warp*n_warp==num_warps` num_warps=0 | - | - | |
| 62 | +| _chunk_state_bwd_dx (B) | YES | FAIL: `Can't cast a handle to other types` | - | - | |
| 63 | +| _chunk_state_bwd_ddAcs_stable (B) | YES | FAIL: num_warps=0 | - | - | |
| 64 | + |
| 65 | +**3/7 compile to real tensor-core MMA CUDA through OUR stack. 1/3 confirmed to launch. |
| 66 | +0/7 confirmed correct (output truncation bug blocks parity).** |
| 67 | + |
| 68 | +## MEASURED (gb10, representative SMALL config) |
| 69 | + |
| 70 | +Config: batch=1, nheads=8, headdim=64, dstate=64, nchunks=8, chunk_size=64, ngroups=1, |
| 71 | +seqlen=512, BLOCK_M=BLOCK_N=64, BLOCK_K=32, HAS_SEQ_IDX=False. |
| 72 | + |
| 73 | +- NATIVE triton `_chunk_scan_bwd_dstates` (inner JITFunction, autotune stripped, BLOCKS pinned), |
| 74 | + per-kernel wall time: **0.0167 ms** (this small config is tiny; the manifest's ~10ms-class |
| 75 | + figure is for the full production-size workload, not this representative slice). |
| 76 | +- ROUTED tilelang->tvm kernel, per-launch wall time: **8.19 ms** — dominated by Python/FFI |
| 77 | + per-call overhead + the MVP SCALAR load/store path (C++ PtrAnalysis shim is disabled |
| 78 | + in-process), NOT a tensor-core-bound number. This is the launch of a kernel that only |
| 79 | + writes ONE tile, so it is NOT a fair perf comparison and MUST NOT be read as "the routed |
| 80 | + kernel is 8ms". |
| 81 | +- path_c v1 905ms: not a meaningful comparison at this stage (different scope; routed kernel |
| 82 | + is per-kernel and output-incorrect). |
| 83 | + |
| 84 | +## Parity vs native (1e-3, all elements): FAIL |
| 85 | + |
| 86 | +Routed output `out_buf`: 32 / 4096 elements nonzero; native fills the full |
| 87 | +`(batch, nchunks, nheads, headdim, dstate) = 262144`-element tensor. allclose(1e-3) = False. |
| 88 | +The routed kernel cannot write past one 64x64 tile. |
| 89 | + |
| 90 | +## ROOT CAUSE (real frontend defect — reported loudly, NOT patched here) |
| 91 | + |
| 92 | +The output buffer `dprev_states_ptr` is size-inferred by the walker as a SINGLE program tile |
| 93 | +`T.Buffer((4096,))` (4096 = hdim*dstate), whereas the INPUT buffers are correctly grid-scaled |
| 94 | +`(2048 * gridDim_0*gridDim_0_1*gridDim_0_2*gridDim_0_3,)`. Two consequences in the generated CUDA: |
| 95 | +1. A spurious store guard `if (idx < 4096) arg2[idx] = ...` (mma.cu line ~607) clamps every |
| 96 | + write whose per-block base offset (`blockIdx.x*arg22 + ...`) lands beyond the first tile — |
| 97 | + so all blocks except the first tile are silently dropped. |
| 98 | +2. The undersized `[4096]` output param vs the real 262144-element tensor causes an FFI/launch |
| 99 | + segfault when a correctly-sized output buffer is passed; passing exactly 4096 avoids the |
| 100 | + crash but yields the truncated result above. |
| 101 | + |
| 102 | +Native triton stores with mask `(offs_m < hdim) & (offs_n < dstate)` ONLY and a per-block base |
| 103 | +pointer offset — it has NO `< 4096` whole-buffer clamp. The clamp is purely an artifact of OUR |
| 104 | +output-buffer size inference treating a masked store's decl_buffer tile-extent as the whole-buffer |
| 105 | +extent. Fix belongs in the frontend output-buffer shape inference (grid-scale the store target |
| 106 | +like the load sources), then re-verify parity. This is a substantive frontend change, out of |
| 107 | +scope for this measure step, and is left UNPATCHED rather than worked around. |
| 108 | + |
| 109 | +## Reproduce |
| 110 | + |
| 111 | +- Capture TTIR (pre-tvm, isolates libtriton): `/tmp/ttir7/*.ttir` (7 kernels). |
| 112 | +- PrimFunc dump: `/tmp/verify_route.py <name>`. |
| 113 | +- Compile + CUDA dump: `/tmp/run_route.py <name>` -> `/tmp/route_cuda/<name>_compiled.cu`. |
| 114 | +- Native ref (isolated proc): `/tmp/phaseA_native.py` -> `/tmp/dstates_io.pt`. |
| 115 | +- Routed launch + measure (isolated proc): `/tmp/phaseB2.py`. |
| 116 | + |
| 117 | +NOTE: native-triton and the routed tvm path CANNOT coexist in one process (both link LLVM |
| 118 | +statically; loading both -> duplicate cl::opt -> segfault). All measurement is two-phase: |
| 119 | +native in one subprocess (saves tensors), routed in a fresh subprocess. |
0 commit comments