|
| 1 | +# Triton-frontend iteration 4 — real 2D-strided `T.copy` CopyNode for the K-loop producer (TMA / UTMALDG) |
| 2 | + |
| 3 | +Status: **HONEST PARTIAL — mechanism PROVEN, default §P1 path NO-GO on UTMALDG.** |
| 4 | +The iteration-4 CopyNode conversion is committed on the routed-triton async path. |
| 5 | +The CopyNode IS recognized by `PipelinePlanning` as a proven copy and, when the |
| 6 | +innermost global stride is provably contiguous, the C-tile lowers to genuine |
| 7 | +**UTMALDG** (TMA bulk async load) on sm_121 — proven by SASS from HEAD. BUT on the |
| 8 | +real §P1 de-monomorphized kernel **neither tile is statically TMA-eligible** |
| 9 | +(dout's innermost axis is the K-reduction/seq axis, stride 7168, genuinely |
| 10 | +non-contiguous; C's innermost is runtime-contiguous but passed as an opaque |
| 11 | +symbolic arg so `is_one()` cannot prove it). Both therefore correctly fall through |
| 12 | +to the existing predicated-`For` path — the generated `.cu` is **byte-identical** |
| 13 | +to iteration-3, UTMALDG stays 0, and there is **no ms drop** from the 1102 ms |
| 14 | +iteration-2/-3 baseline. |
| 15 | + |
| 16 | +All numbers below are MEASURED-FROM-COMMITTED-HEAD on gb10 (sm_121a), regenerating |
| 17 | +the `_chunk_scan_bwd_dstates` artifacts from HEAD, interleaved OFF/OPT, parity |
| 18 | +re-verified under the GPU mutex. |
| 19 | + |
| 20 | +- tilelang committed HEAD: `1378c1737b0dda677a24b3808801ed24745b610a` |
| 21 | + (`merge/upstream-codegen-reorg`). gb10 working tree byte-identical to HEAD for |
| 22 | + all 4 routed-frontend files (md5-verified: `memory.py 5d6f289d…`, |
| 23 | + `control.py 5c7f9cb9…`, `__init__.py 31ebe2f4…`, `op_mapping.py 3648f32f…`). |
| 24 | + |
| 25 | +## What landed (committed, iteration 4) |
| 26 | + |
| 27 | +`poc/triton_frontend/op_emitters/memory.py`: |
| 28 | + |
| 29 | +- `_ptrstate_block_base_and_strides` — splits the collapsed PtrState flat address |
| 30 | + into a per-block `elem_offset` (loop-independent offsets + carry advance) and |
| 31 | + per-axis strides, preserving the 2D axis-contiguity that the 1D flat-index |
| 32 | + lowering had discarded. |
| 33 | +- `_emit_ptrstate_tile_load_copynode` — declares the global source (dout/C) as a |
| 34 | + 2D `tir.Buffer` aliasing the flat arg `data` Var with `strides=[stride0,stride1]` |
| 35 | + + `elem_offset=base`, emits a REAL `T.copy(src2d -> shared_tile)` CopyNode, and |
| 36 | + SPLITS the bounds mask OFF into a separate masked epilogue (not inside the |
| 37 | + `if_then_else` producer). |
| 38 | +- A **TMA-eligibility gate (RULE #1)**: the CUDA bulk-copy lowering hard-requires |
| 39 | + a statically-provable contiguous innermost global stride |
| 40 | + (`ICHECK(is_one(desc.global_stride[0]))` in `src/backend/cuda/op/copy.cc`). If |
| 41 | + the innermost stride is provably 1 (or `routed_contiguous_innermost` supplies |
| 42 | + ground-truth contiguity) the CopyNode is emitted; otherwise the function |
| 43 | + RETURNS FALSE and the caller keeps the load on the predicated-`For` path |
| 44 | + WITHOUT stamping `num_stages` — no predicated-For dressed up as a coalesced copy. |
| 45 | + |
| 46 | +Wired into `_emit_ptrstate_tile_load_tir`, gated on |
| 47 | +`routed_triton_async_loads and rank>=2`. The iteration-3 infra |
| 48 | +(`async_loads` gate, `_gmem_shared_copies` counter, `map_scf_for._maybe_pipeline` |
| 49 | +`num_stages` stamp) is the committed prerequisite. |
| 50 | + |
| 51 | +## Measured — default §P1 path (gb10, `_chunk_scan_bwd_dstates`) |
| 52 | + |
| 53 | +§P1 config: b=1 nh=112 hd=64 ds=64 nc=64 cs=64, BM/BN/BK = 64/64/32. |
| 54 | + |
| 55 | +| metric | async OFF | async ON (HEAD default) | iter-3 baseline | Triton ref | |
| 56 | +|---|---|---|---|---| |
| 57 | +| generated `.cu` md5 | `99c232418b10c2793928ed63776b76c2` | `99c232418b10c2793928ed63776b76c2` | `99c23241…` | — | |
| 58 | +| `.cu` byte-identical OFF==ON | — | **True** | True | — | |
| 59 | +| SASS UTMALDG | 0 | **0** | 0 | — | |
| 60 | +| SASS LDGSTS | 0 | 0 | 0 | (Ampere-only) | |
| 61 | +| SASS LDG | 38 | 38 | 38 | — | |
| 62 | +| SASS spill (STL+LDL) | 306 (168+138) | 306 (168+138) | 306 | 0 | |
| 63 | +| SASS HMMA | 32 | 32 | 32 | 256 | |
| 64 | +| §P1 parity MAXDIFF | — | **4.882812e-04** | 4.88e-04 | — | |
| 65 | +| ALLCLOSE 1e-3 | — | **PASS** | PASS | — | |
| 66 | +| EXEC ms/kernel (CUDA events, N=50, ×4 interleaved) | median **1101.79** | median **1101.41** | ~1102 | ~1.12 | |
| 67 | +| Δ ms (OFF−OPT) | — | **0.38** (noise) | 0 | — | |
| 68 | + |
| 69 | +OFF per-rep: 1101.59 / 1101.99 / 1102.38 / 1101.25. |
| 70 | +OPT per-rep: 1101.19 / 1101.35 / 1102.03 / 1101.47. |
| 71 | +The `.cu` is byte-identical OFF==ON → the 0.38 ms is run-to-run noise. **No real |
| 72 | +drop from 1102.** Small real-strided multi-K-trip parity (parity_all7 `small`: |
| 73 | +b1 nh8 hd64 ds64 nc4 cs64 ng2 s256, 2 K-trips, real strides): MAXDIFF=**3.051758e-05**, |
| 74 | +ALLCLOSE 1e-3 PASS — no regression from baseline. |
| 75 | + |
| 76 | +## Mechanism PROOF — CopyNode → UTMALDG fires when the innermost is contiguous |
| 77 | + |
| 78 | +Reproducible from HEAD by supplying ground-truth contiguity |
| 79 | +(`ctx.routed_contiguous_innermost=True`, the flag the route would set once it has |
| 80 | +verified the real tensor's innermost stride == 1) and dropping `disable_tma`. The |
| 81 | +C-tile (`[k, ds]`, innermost = ds) then pins a literal-1 innermost and the CopyNode |
| 82 | +takes the TMA bulk path: |
| 83 | + |
| 84 | +| metric | default §P1 | ground-truth-contiguous C-tile | |
| 85 | +|---|---|---| |
| 86 | +| `.cu` `CUtensorMap` | 0 | **2** | |
| 87 | +| `.cu` `mbarrier` | 0 | **22** | |
| 88 | +| `.cu` `swizzle` | 0 | **1** | |
| 89 | +| SASS UTMALDG | 0 | **6** | |
| 90 | +| SASS UTMACCTL | 0 | **1** | |
| 91 | +| SASS LDG | 38 | **32** (the 6 C-tile loads moved to TMA) | |
| 92 | +| SASS HMMA | 32 | 32 (intact) | |
| 93 | + |
| 94 | +This proves the CopyNode→`cuda::Copy::LowerBulk`→UTMALDG path is real and live on |
| 95 | +sm_121 — the missing piece was a statically-provable contiguous innermost, not the |
| 96 | +pipeline plumbing. |
| 97 | + |
| 98 | +## RULE #1 — honest TMA-eligibility of the §P1 tiles |
| 99 | + |
| 100 | +- **dout tile** is `[hd, k]` → innermost = the **K-reduction (seq) axis**, runtime |
| 101 | + stride `nheads*headdim = 112*64 = 7168`. **Genuinely non-contiguous → TMA-INELIGIBLE.** |
| 102 | + TMA needs the innermost axis contiguous; dout does not qualify. Correct |
| 103 | + fall-through to predicated-`For` (NOT counted as an async copy). |
| 104 | +- **C tile** is `[k, ds]` → innermost = ds, runtime stride 1 (contiguous), BUT the |
| 105 | + de-monomorphized kernel passes it as opaque `arg19`, so `is_one()` cannot prove |
| 106 | + `== 1` at compile time → TMA-INELIGIBLE on the default path. The mechanism proof |
| 107 | + above shows it DOES take UTMALDG once contiguity is ground-truthed. |
| 108 | + |
| 109 | +No silent fallback was introduced: when a tile is not provably TMA-eligible the |
| 110 | +CopyNode emitter RETURNS FALSE and the load stays on the correct predicated-`For` |
| 111 | +path, and the K-loop is NOT mis-annotated with `num_stages`. Parity holds at §P1 |
| 112 | +(MAXDIFF 4.88e-04, unchanged) and small multi-K-trip (3.05e-05). The GEMM |
| 113 | +cooperative path is byte-intact. |
| 114 | + |
| 115 | +## Other paths (no-regression) |
| 116 | + |
| 117 | +`route_all7`: 5/7 route with mma=2 intact (dstates / dc / state_db / state_dx / |
| 118 | +ddAcs). The 2 fails (`dcb`: PtrState unresolved SSA `%1044`; `dx`: undefined |
| 119 | +`arg4_numel_147`) are PRE-EXISTING and IDENTICAL to baseline — not a regression. |
| 120 | +GEMM HMMA=32 intact in the routed kernels. |
| 121 | + |
| 122 | +## GO / NO-GO |
| 123 | + |
| 124 | +- **GO** on the CopyNode mechanism: the real 2D-strided `T.copy` is recognized as a |
| 125 | + proven copy and lowers to genuine UTMALDG (SASS UTMALDG=6, LDG 38→32) when the |
| 126 | + innermost is contiguous — proven from HEAD. |
| 127 | +- **NO-GO** on a §P1 ms drop this iteration: both §P1 tiles are statically |
| 128 | + TMA-ineligible (dout genuinely non-contiguous innermost; C contiguous but |
| 129 | + opaque-symbolic), so the default path correctly falls through, `.cu` is |
| 130 | + byte-identical, UTMALDG=0, EXEC stays 1101 ms. |
| 131 | + |
| 132 | +Remaining gap to native ~1.12 ms is ~983x, now precisely localized to TWO |
| 133 | +prerequisites the frontend cannot satisfy alone: (1) the de-monomorphized launch |
| 134 | +must surface a literal-1 innermost stride for C (monomorphization / a |
| 135 | +route-supplied contiguity contract), and (2) dout needs a transposed tile so its |
| 136 | +innermost becomes the contiguous headdim axis rather than the seq axis. With both, |
| 137 | +C+dout take UTMALDG; HMMA (32 vs Triton 256) and spill (306) remain the next |
| 138 | +fronts after the loads coalesce. |
0 commit comments