|
| 1 | +# Generic GEMM tiling fix: honoring the Triton autotune warp/stage config |
| 2 | + |
| 3 | +Status: MEASURED on NVIDIA GB10 (sm_121, CUDA 13.2) under the gb10 GPU mutex. |
| 4 | +Branch: `merge/upstream-codegen-reorg`. Fix commit: `01a2a7e6`. |
| 5 | + |
| 6 | +## Problem (root cause) |
| 7 | + |
| 8 | +The routed-triton `from_ttir` path (`poc/triton_frontend/__init__.py`) defaulted |
| 9 | +`ctx.num_warps = 4` (a 128-thread block) for EVERY routed kernel, even when the |
| 10 | +source Triton kernel autotuned to a different warp count. For the §P1 |
| 11 | +`_chunk_scan_bwd_dstates_kernel` the native Triton autotuner selects |
| 12 | +`num_warps=8, num_stages=3`. With only 4 warps, the cooperative `T.gemm` tiles |
| 13 | +the SAME single `tt.dot` over a 4-way warp partition instead of the 8-way |
| 14 | +partition Triton emits, so the 64x64 dstates tile is partitioned 4x too small |
| 15 | +and the per-thread register pressure explodes (heavy spilling). |
| 16 | + |
| 17 | +`ctx.num_warps` flows, unchanged, through `op_mapping.map_tt_func` |
| 18 | +(`op_mapping.py:1037`): it sets the block `threadIdx.x` `thread_extent = |
| 19 | +num_warps*32` and stamps the PrimFunc `num_warps` attr. That thread extent is |
| 20 | +exactly what `tilelang`'s backend-agnostic |
| 21 | +`GemmWarpPolicy.compute_warp_partition(M, N, num_warps)` |
| 22 | +(`tilelang/tileop/base.py:65`) reads to derive the `(m_warp, n_warp)` |
| 23 | +partition. For a 64x64 tile: at `num_warps=8` the square policy finds |
| 24 | +`m_warp*n_warp=8` (e.g. 4x2); at `num_warps=4` it can only reach 2x2/4x1. |
| 25 | + |
| 26 | +## Fix (GENERIC, backend-agnostic, no per-kernel hack) |
| 27 | + |
| 28 | +`from_ttir` now reads the autotuned config when the caller does not pass an |
| 29 | +explicit `num_warps`/`num_stages`. New `_read_ttir_warp_config()` |
| 30 | +(`__init__.py:1444`) probes the TTIR module operation's MLIR attributes that |
| 31 | +Triton stamps after binding the selected `triton.Config`: |
| 32 | +`ttg.num-warps` / `num-warps` / `num_warps` and the matching `*-stages` keys. |
| 33 | + |
| 34 | +- Any TTIR carrying these standard module attrs is honored -- not a per-kernel |
| 35 | + special case. Each kernel gets ITS OWN autotuned config: `dstates` and `dx` |
| 36 | + resolve to `num_warps=8`; `dc`/`dcb` resolve to `num_warps=4` (correctly kept |
| 37 | + small). |
| 38 | +- Explicit `num_warps`/`num_stages` kwargs always win (the documented override |
| 39 | + used by the text-TTIR §P1 harness, which carries no module attrs). |
| 40 | +- RULE #1 (fail loud): a present-but-malformed or non-positive warp attr RAISES |
| 41 | + rather than silently falling back to the 4-warp default. |
| 42 | + |
| 43 | +`ctx.num_warps` then flows into `gemm.lower`'s `computeWarpPartition` on CUDA and |
| 44 | +the threadgroup partition on Metal alike -- the fix lives entirely in the |
| 45 | +backend-agnostic frontend + the existing tilelang Python tile-op layer. No |
| 46 | +C++ core change, no libtriton dependency, no cuda-only op. Python-frontend only; |
| 47 | +no gb10 rebuild required. |
| 48 | + |
| 49 | +## Measured effect (EXECUTED on gb10, raw §P1 dstates TTIR, prologue_opt + cp.async) |
| 50 | + |
| 51 | +Build-only SASS census of the EXACT compiled cubins (same raw TTIR, prologue_opt |
| 52 | +=True, TL_FORCE_CP_ASYNC=1; only the warp/stage config differs): |
| 53 | + |
| 54 | +| config | HMMA | LDGSTS | IMAD | ISETP | spill (STL+LDL) | |
| 55 | +|--------------------------------|------|--------|------|-------|-----------------| |
| 56 | +| default (num_warps=4) | 32 | 0 | 1250 | 926 | 1596 | |
| 57 | +| TileFix autotune (num_warps=8) | 16 | 8 | 788 | 796 | 272 | |
| 58 | +| native Triton (reference) | -- | 75 | 298 | 69 | 0 | |
| 59 | + |
| 60 | +- spill 1596 -> 272 (5.9x fewer spill instructions): the 8-way warp partition |
| 61 | + cuts per-thread register pressure substantially. |
| 62 | +- LDGSTS 0 -> 8: cp.async/LDGSTS becomes live at 8 warps (the wider tile lets |
| 63 | + the multi-stage copy loop pipeline). |
| 64 | +- IMAD 1250 -> 788, ISETP 926 -> 796: residual addressing moves toward native |
| 65 | + (298/69). Note HMMA SASS *count* DROPS with more warps -- it is a per-thread |
| 66 | + instruction count, so an 8-way partition issues fewer HMMA per thread for the |
| 67 | + same block-level work; HMMA count is NOT a direct "256-HMMA" proxy. |
| 68 | + |
| 69 | +Parity: both default and TileFix configs match the native kernel bit-for-bit at |
| 70 | +MAXDIFF = 4.882812e-04 (PASS). |
| 71 | + |
| 72 | +Timing (CUDA events, N=50 x 4 reps, interleaved, median-of-medians): see the |
| 73 | +TFARESULT / WTRESULT lines in the run logs on gb10. |
| 74 | + |
| 75 | +## Generic proof (2nd kernel) |
| 76 | + |
| 77 | +`_chunk_scan_bwd_dx_kernel` ALSO autotunes to `num_warps=8` (its best |
| 78 | +`triton.Config`). Honoring its autotune config (vs the old default-4) drops its |
| 79 | +spills the same way -- the fix reads each kernel's own config, not a dstates |
| 80 | +hack. `dc`/`dcb` autotune to 4 warps and the reader correctly keeps them at 4. |
| 81 | + |
| 82 | +## Reproduce from HEAD |
| 83 | + |
| 84 | +``` |
| 85 | +ssh gb10 |
| 86 | +cd /home/dave/source/tilelang # HEAD 01a2a7e6 (merge/upstream-codegen-reorg) |
| 87 | +python /tmp/tilefix_final_ab.py # BASE(default-4) vs FIX(autotune-8) + native |
| 88 | +python /tmp/dx_generic_probe.py # 2nd kernel (dx) generic proof |
| 89 | +``` |
0 commit comments