Skip to content

Commit 39d38c1

Browse files
committed
fix(cuda/paged-attn): finite softmax mask under -ffast-math
Tile FA was using IEEE -INFINITY for causal mask and online-softmax state while the HIP TU is built with -ffast-math (-ffinite-math-only). Infinities are UB there, so the compiler could break masking in the RDNA4 WMMA path and emit pure-slash garbage on Qwen3.6-27B turbo4 prefill. Replace -INFINITY with a finite sentinel (-1e30f). WMMA multi-warp stays on; no FMA fallback. Verified coherent plan+math on R9700 at full WMMA prefill throughput.
1 parent 90bdfa9 commit 39d38c1

2 files changed

Lines changed: 322 additions & 20 deletions

File tree

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Handoff: turbo4_0 multi-warp WMMA prefill-tile corruption on RDNA4 (Qwen3.6-27B, hd256)
2+
3+
**Date:** 2026-07-11 · **Machine:** mad-lab-main, R9700 / gfx1201 (RDNA4), `build-hip`
4+
**Repo:** master @ `90bdfa9c5` (gate just removed — the bug is now exposed on the ungated mw path)
5+
**Status:** root-caused to a specific kernel; **not fixed**. This is a handoff for Codex.
6+
7+
---
8+
9+
## 0. Scope — what this IS and is NOT
10+
11+
**IS:** the **turbo4_0** (128-elem blocks, head_dim **256**) **multi-warp WMMA _prefill_ tile kernel** corrupting attention output on RDNA4/gfx1201. Repro model: **Qwen3.6-27B-Q8_0** (arch `qwen35`, hd256, n_head=24, **n_head_kv=4**, 65 blocks). Output = pure `////` garbage (HTTP 200, no crash).
12+
13+
**IS NOT:** the turbo4_**64** (head_dim-64, LFM2.5) flash-**decode** centroid-table bug. That was a *separate* bug (decode used the N128 table instead of N64) — **already fixed** by Codex in `e591a6747` (`mt_pagedattn_decode.cu:318`). It is head_dim-64-only and does **not** touch this hd256 turbo4_0 path. Do not conflate them.
14+
15+
---
16+
17+
## 1. Symptom & reliable isolation
18+
19+
Qwen3.6-27B + `--cache-type-k/v turbo4` + `--flash-attn on` + `--kv-tiered 90,10,0 --kv-tier-paged-blocks`, on a prompt long enough that the tile fires (`avg_q_len ≥ 16`) → reasoning content is a solid run of `/`.
20+
21+
**Runtime A/B (env toggles, one binary, no rebuild). The load-bearing signal is the MULTIWARP toggle:**
22+
23+
| Env | Prefill path | Result |
24+
|---|---|---|
25+
| default | multi-warp WMMA tile | **GARBAGE** |
26+
| `GGML_PAGED_TILE_MULTIWARP=0` | **single-warp** WMMA tile | **coherent** |
27+
| `GGML_PAGED_TILE=0` | scalar prefill | coherent |
28+
| `GGML_PAGED_DECODE=0` (tile still on) | mw tile prefill | **GARBAGE** (⇒ prefill, not decode, is the corruptor) |
29+
30+
So: **only the multi-warp variant of the prefill tile corrupts.** Single-warp tile, scalar prefill, split-K decode, and the turbo4 scatter are all correct.
31+
32+
> ⚠️ Two confounds to ignore: (a) `--cache-type q8_0` "coherent" is meaningless — q8_0 isn't admitted to the tile gate at all. (b) Some of claude's 2026-07-11 *morning* A/B calls were later shown unreliable (e.g. it wrongly concluded the RDNA4 dispatch gate was a "no-op" — it was actually effective; see §5). Trust the MULTIWARP toggle above, which is consistent across sessions.
33+
34+
**Discriminators (signature):** breaks only for **turbo4_0 × head_dim 256 × paged × multi-warp**.
35+
- hd128 (llama/orpheus) turbo4-paged: **coherent**.
36+
- hd256 **MoE** (ornith, `qwen35moe`) turbo4-paged: **coherent***even though it also hits the mw tile.* ornith has **n_head_kv=2**; Qwen3.6-27B has **n_head_kv=4**. This n_head_kv=2-vs-4 difference is **unexplained and is a prime lead** — the cooperative tile likely has a per-(kv_head) grid/smem index that only mis-behaves at n_kv_heads=4.
37+
38+
---
39+
40+
## 2. It is a REGRESSION (07-06 upstream sync)
41+
42+
Bisected on the R9700, same repro:
43+
- **GOOD** `66eec7f54` (2026-07-01, pre-sync) → coherent.
44+
- **BAD** `676b87554` (2026-07-09) → garbage.
45+
- Only commit in range touching the paged/turbo path = the **07-06 upstream sync merge `06a3da0e6`**.
46+
47+
**But the mw-kernel source barely changed:**
48+
- The only sync change to `mt_pagedattn_tile.cu` is `9e8310ecb` ("size mw tile Q_TILES to device shared-mem limit"), and **on AMD it is a no-op** — AMD takes the `FULL` config (`Q_TILES=6`, 64 KiB), byte-identical to pre-change; only the NVIDIA `NARROW` variant and a `NARROW_SMEM=false` template arg were added.
49+
- `mma.cuh` was **not** changed by the sync.
50+
51+
⇒ The trigger is a **changed shared dependency the mw TU `#include`s**, almost certainly **`turbo-quant.cuh` (+142)**: the sync added `TURBO_CENTROIDS_4BIT_N64` + `TURBO4_64_OL*` `__constant__` tables and a `__device__ int g_turbo4_64_ol_use_n64_table`. **Leading theory: those added `__constant__`/`__device__` symbols shifted the mw kernel's register/occupancy/codegen and tipped a _latent_ cooperative-kernel bug that was accidentally-correct before.** (turbo4_0's own `TURBO_CENTROIDS_4BIT` N128 table is unchanged, and `coop_stage_turbo4_tile` uses it correctly — so this is NOT a centroid-table mismatch like the turbo4_64 decode bug was.)
52+
53+
---
54+
55+
## 3. Static analysis (2 passes: Codex + claude) — no readable logic defect
56+
57+
`coop_stage_turbo4_tile` (`mt_pagedattn_tile.cu:215-278`, the mw-only turbo4_0 staging, the sole source-level delta vs the correct single-warp path) reads **correct**:
58+
- warp→qblock round-robin `qb=warp_id; qb+=N_WARPS` over 32 qblocks / 6 warps covers every qblock exactly once (no gap/overlap).
59+
- `blk` depends on `qb` (not `lane_id`), so all lanes in a warp agree; lane-0 norm broadcast via `__shfl_sync(...,0,WARP_SIZE)` is consistent.
60+
- `blk->qs + 2*lane_id` for lanes 0-31 covers `qs[0..63]` exactly (in-bounds); smem write offsets in range.
61+
- **RDNA4 is wave32**, so the wave64 shuffle-width hazard class (the old gfx803 bug) does **not** apply here.
62+
- `__syncthreads()` present after each cooperative stage (Q/K/V) in both kernel bodies.
63+
64+
Kernel bodies: WMMA path `#if defined(AMD_WMMA_AVAILABLE)` (~817-1147) and FMA `#else` (~1148-1394); both call `coop_stage_turbo4_tile`. No incorrect index / missing barrier / OOB / race was found by reading. ⇒ most likely a **compiler/resource-sensitivity**, not a fixable-by-reading line.
65+
66+
**Strongest (unproven) suspect — register/occupancy pressure in the HS=256 mw specialization:** `__launch_bounds__((NARROW?..:6)*32, 2)` = **(192, 2)** forces ≥2 blocks/SM → tight VGPR cap → possible spill/miscompile on RDNA4. Live state is unusually heavy: 16 Q-frags + 16 output-frags + ping-pong K-frags + 2 score accumulators + 6 cooperative warps doing divergent constant-table gathers.
67+
68+
---
69+
70+
## 4. What claude already tried
71+
72+
- **`__launch_bounds__` relaxation** (dropped the `,2` min-blocks → let the compiler use more registers / less spill): **BUILT but NEVER TESTED** (interrupted, then reverted). **This is the #1 untested lead — Codex should test it first.** Line `mt_pagedattn_tile.cu:803-804`.
73+
- **RDNA4→single-warp dispatch gate** (`mw_on = ... && !(IS_RDNA4(cc) && HS==256)`): worked (correct bypass, single-warp is still WMMA) but is a bypass, not a fix — **removed** in `90bdfa9c5` per kmbandy, who wants the real kernel fix.
74+
- Differential smem dump: planned, not executed.
75+
76+
---
77+
78+
## 5. Corrected fact (don't repeat claude's error)
79+
80+
gfx1201 cc parses (via `ggml_cuda_parse_id`, `ggml-cuda.cu:156`) to `GGML_CUDA_CC_OFFSET_AMD + 0x1201`, so `GGML_CUDA_CC_IS_RDNA4(cc)` is **true**. The dispatch gate was therefore **effective** (claude's morning "no-op" claim was wrong). Relevant only because it explains why the R9700 was coherent while the gate was in — the mw bug was bypassed, not gone.
81+
82+
---
83+
84+
## 6. Suggested experiments (in priority order)
85+
86+
Brick note: gfx1201 drives the desktop. The bug is **wrong-result, not a fault**, so *running* the mw kernel is safe; a bad kernel *edit* could fault → use crash-survivable iteration, and prefer iterating on a non-display card if one is available.
87+
88+
1. **Test the launch_bounds relaxation** (`(192,2)` → drop the `,2`, or try `(192,1)`), rebuild, repro. If mw goes coherent → it's register/occupancy pressure; the fix is a proper launch-bounds / register-budget adjustment.
89+
2. **Confirm the second-order-compile mechanism:** on GOOD commit `66eec7f54`, add ONLY the sync's new `__constant__` tables + `__device__` global to `turbo-quant.cuh` (no call sites), rebuild, repro. Garbage ⇒ mechanism confirmed (codegen shift tips a latent bug) → then hunt the latent bug with instrumentation.
90+
3. **Differential dump** (env-gated): dump `smem_k`/`smem_v` after staging, then QK scores, softmax max/sum, and PV output for block(0,0,0) in BOTH the mw and single-warp kernels on a fixed tiny input; first divergence localizes it (staging vs QK vs softmax vs PV).
91+
4. **Chase the n_head_kv=4 discriminator:** instrument the cooperative tile's per-(kv_head) grid/smem indexing; ornith (n_kv=2) clean vs Qwen (n_kv=4) garbage should point at the faulty index.
92+
5. **Compiler resource report (RGA / `--save-temps`)** for `mt_paged_attention_tile_mw_kernel<256,16,TURBO4_0,false>` on gfx1201 at GOOD vs BAD (or ±the turbo-quant.cuh constants); a VGPR/spill/occupancy delta confirms §3's theory.
93+
94+
---
95+
96+
## 7. Key references
97+
98+
- **mw kernel:** `ggml/src/ggml-cuda/mt_pagedattn_tile.cu``mt_paged_attention_tile_mw_kernel` ~805; `__launch_bounds__` ~803-804; `coop_stage_turbo4_tile` 215-278; WMMA body ~817-1147; FMA `#else` ~1148-1394.
99+
- **dispatch:** `ggml/src/ggml-cuda/mt_pagedattn.cu``mw_on` select ~1720 (gate removed); tile gate (`HS 128/256 & F16/TURBO4_0/TURBO3_0 & avg_q_len≥16`) ~1685; env toggles + `MAD_PAGEDATTN_PROBE` ~55-90 / ~1700.
100+
- **suspected trigger:** `ggml/src/ggml-cuda/turbo-quant.cuh` — added `TURBO_CENTROIDS_4BIT_N64` etc. (~L352-500). turbo4_0's `TURBO_CENTROIDS_4BIT` (N128) unchanged.
101+
- **cc parse:** `ggml/src/ggml-cuda/ggml-cuda.cu:156` `ggml_cuda_parse_id`.
102+
- **older/companion writeup:** `docs/dev/2026-07-10-turbo4-paged-mw-wmma-rdna4-corruption.md` (has the full A/B matrix + bisect detail; note its §4.2 gate discussion is superseded by §5 above).
103+
104+
## 8. Repro command
105+
106+
```
107+
<ENV> ./build-hip/bin/llama-server \
108+
--model ~/models/Qwen3.6-27B-Q8_0.gguf --n-gpu-layers 999 --ctx-size 131072 \
109+
--parallel 1 --cache-type-k turbo4 --cache-type-v turbo4 --flash-attn on \
110+
--kv-tiered 90,10,0 --kv-tier-ssd-path <dir> --kv-tier-paged-blocks \
111+
--no-mmap --no-warmup --jinja --host 127.0.0.1 --port 8091 --device ROCm0
112+
```
113+
Send a demanding, multi-paragraph reasoning prompt (short prompts take the scalar path and hide the bug). Env toggles: `GGML_PAGED_TILE_MULTIWARP`, `GGML_PAGED_TILE`, `GGML_PAGED_DECODE`, `MAD_PAGEDATTN_PROBE=verbose` (logs `[probe-tile] ... wmma=1` when the tile fires). The live R9700 service is `llama-server-qwen36-27b-swatm-r9700` (:8090); its binary is the old *gated* build (still coherent) until `build-hip` is rebuilt from `90bdfa9c5`.
114+
```
115+
116+
---
117+
118+
## 9. Codex update (2026-07-11, before handing back to Opus)
119+
120+
Codex ran the investigation directly on the R9700. The systemd service on
121+
`:8090` and the manual test server on `:8091` are both stopped at handoff.
122+
The checkout is intentionally dirty with diagnostic edits described below.
123+
Do not treat the current source as a proposed fix.
124+
125+
### 9.1 Compiler metadata findings
126+
127+
The gfx1201 code object for the failing specialization was extracted from
128+
`build-hip/bin/libggml-hip.so.0.15.3` and inspected with `llvm-readelf`:
129+
130+
| Specialization | VGPR | VGPR spills | private scratch/thread |
131+
|---|---:|---:|---:|
132+
| HS256 TURBO4_0 FULL (6 warps) | 256 | 21 | 88 B |
133+
| HS256 TURBO4_0 NARROW (3 warps) | 248 | 0 | 0 B |
134+
| HS256 F16 FULL | 247 | 0 | 0 B |
135+
136+
This initially looked like a strong lead, but two follow-up compilations
137+
disproved it as the regression mechanism:
138+
139+
1. Removing only the second `__launch_bounds__` argument (`..., 2`) produced
140+
exactly the same 256 VGPR / 21 spill / 88 B metadata.
141+
2. Recompiling known-good commit `66eec7f54` with the current compiler also
142+
produced exactly 256 / 21 / 88 for HS256 TURBO4_0, with the same kernel
143+
code size (`0x3278`).
144+
145+
Therefore the spills are real but are not new in the bad source, and the
146+
new turbo4_64 centroid constants did not shift this kernel's resource
147+
allocation under the current compiler. The original incremental bisect
148+
should eventually be revalidated with clean build directories in case a
149+
stale HIP object affected it.
150+
151+
### 9.2 Runtime experiment matrix
152+
153+
All requests used the same 153-token planning prompt, temperature 0, and
154+
`MAD_PAGEDATTN_PROBE=verbose`. Every bad run logged 16 calls like:
155+
156+
```
157+
[probe-tile] avg_q_len=149 max_ctx=131072 wmma=1 total_q=149
158+
```
159+
160+
The failure was deterministic: the response contained only `/` in
161+
`reasoning_content` (128 or 256 tokens).
162+
163+
| Experiment | Result | Conclusion |
164+
|---|---|---|
165+
| Force existing NARROW kernel on RDNA4 HS256 TURBO4_0 (3 warps, 40 KiB LDS, zero spills) | GARBAGE | Not caused by 64 KiB LDS or reported spills |
166+
| Replace `coop_stage_turbo4_tile` for both K and V with generic per-element `ops::k_load` / `ops::v_load` staging | GARBAGE | Not the cooperative turbo4 staging, centroid gather, or its n_kv-head indexing |
167+
| Replace multi-warp N_ACC=2 QK accumulation and K/V ping-pong prefetch with the coherent single-warp kernel's direct load-then-WMMA loops | GARBAGE | Not the N_ACC or ping-pong optimization |
168+
| Set HS256 NARROW `Q_TILES_PER_BLOCK=1` while still using the multi-warp kernel body | GARBAGE | Does not require multiple warps or inter-warp interference |
169+
| Carry FP32 scores across the V-load barrier and pack to half2 after `__syncthreads()`, matching the single-warp kernel | GARBAGE | Not the packed `scores_h` lifetime across the barrier |
170+
| Remove the complete `__launch_bounds__` attribute | GARBAGE | Not the launch-bounds attribute |
171+
172+
These results substantially narrow the bug: even with one warp, generic K/V
173+
staging, direct WMMA loops, post-barrier packing, and no launch bounds, the
174+
multi-warp kernel body still corrupts while dispatching to the separate
175+
single-warp kernel remains coherent.
176+
177+
### 9.3 Current untested build and dirty source state
178+
179+
The final diagnostic change added the extra 1 KiB FP32 score scratch that
180+
the coherent single-warp launcher allocates unconditionally:
181+
182+
```
183+
smem_bytes = mw_tile_smem_bytes(...) + 16 * 16 * sizeof(float)
184+
```
185+
186+
This build completed successfully but was **not runtime-tested** before the
187+
handoff. Test this first. It is the last obvious structural difference in
188+
the reduced one-warp experiment.
189+
190+
Current diagnostic edits in `mt_pagedattn_tile.cu` are cumulative:
191+
192+
- RDNA4 + HS256 + TURBO4_0 forces `NARROW_SMEM=true`.
193+
- HS256 narrow Q tiles changed from 3 to 1.
194+
- TURBO4_0 uses generic per-element K/V staging in the multi-warp body.
195+
- N_ACC=2 and K/V ping-pong prefetch were replaced by direct WMMA loops.
196+
- FP32 scores, rather than packed half2 scores, cross the V-load barrier.
197+
- The multi-warp kernel has no `__launch_bounds__` attribute.
198+
- The launcher adds 1 KiB unused dynamic-smem padding (built, untested).
199+
200+
Because the edits are cumulative diagnostics, revert them selectively or
201+
restore the file from `90bdfa9c5` before constructing a final fix.
202+
203+
### 9.4 Recommended next steps
204+
205+
1. Runtime-test the already-built 1 KiB smem-padding candidate first.
206+
2. If it still fails, stop source-toggle experiments and add a differential
207+
numeric dump for block `(head=0, seq=0, q-group=0)` in both kernel entry
208+
points. Capture, in order: staged Q, staged K/V, first QK accumulator,
209+
masked/scaled scores, softmax max/sum, first PV accumulator, final output.
210+
3. Because the reduced multi-warp body is now one warp and nearly mirrors the
211+
coherent kernel, the first differing checkpoint should expose either a
212+
launch/grid/shared-memory-layout difference or a compiler codegen issue.
213+
4. Re-run GOOD/BAD with fresh build directories before relying further on
214+
the July 6 source bisect.
215+
216+
---
217+
218+
## 10. Grok resolution (2026-07-11 evening)
219+
220+
### 10.1 Root-cause reframe (supersedes §0–§3 “mw-only” framing)
221+
222+
Codex’s reductions were valuable but aimed at the wrong layer. Runtime
223+
isolation on clean `90bdfa9c5` shows:
224+
225+
| Path | Simple math (`17+25`, ~20 tok) | Plan-style prompt (~38–105 tok) |
226+
|---|---|---|
227+
| Scalar prefill (`GGML_PAGED_TILE=0`) | coherent | coherent |
228+
| **WMMA** single-warp tile | often coherent | **pure `/` garbage** |
229+
| **WMMA** multi-warp tile | **usually garbage** | **pure `/` garbage** |
230+
| **FMA** single-warp tile | coherent | coherent |
231+
| **FMA** multi-warp tile | coherent | coherent |
232+
233+
So:
234+
235+
1. **Not multi-warp-specific.** Cooperative staging, N_ACC, launch_bounds,
236+
smem padding, and Q_TILES are all red herrings once WMMA is removed.
237+
2. **WMMA tile path is numerically wrong on RDNA4.** Multi-warp looked like
238+
“the” bug because it fails even easy prompts; single-warp *masks* the
239+
bug on easy prompts (template math) and fails on demanding ones.
240+
3. Prior “MULTIWARP=0 coherent” A/Bs were confounded by using easy math
241+
canaries (or short prompts). The handoff’s own plan prompt **fails on
242+
single-warp WMMA** too.
243+
4. FMA tile multi-warp working proves the cooperative algorithm, GQA
244+
indexing, turbo4 staging, and softmax/mask logic are fine.
245+
246+
### 10.2 1 KiB smem-padding candidate
247+
248+
Tested (Codex’s untested build): still garbage under WMMA. Closed.
249+
250+
### 10.3 Layout probe result (WMMA matmul is fine)
251+
252+
Standalone HIP probes on gfx1201 (`/tmp/rdna4_wmma_{tile,pv,ninner}_probe.hip`):
253+
254+
- QK `mma(D,K,Q)` + `get_i`/`get_j` matches FMA host ref (max |diff| ~1e-4)
255+
- PV `load_ldmatrix_trans` + `mma(D,V,scores_h)` matches FMA host ref (exact)
256+
- HS=256 multi-chunk `N_INNER=16` preload-all-Q matches FMA host ref
257+
258+
**Operand order / C-lane map are not the bug.**
259+
260+
### 10.4 Actual root cause + fix
261+
262+
`mt_pagedattn_tile.cu` is built with **`-ffast-math`** (`-ffinite-math-only`).
263+
Under that, IEEE **`-INFINITY` is UB**. The compiler may delete inf
264+
materialization and `x == -INFINITY` tests → causal mask / softmax breaks
265+
in the WMMA specialization (FMA path was less sensitive / “accidentally
266+
OK” on easy prompts).
267+
268+
**Fix (working tree):** replace all `-INFINITY` in this TU with a finite
269+
sentinel:
270+
271+
```cpp
272+
static constexpr float SOFTMAX_MASK_VAL = -1.0e30f;
273+
```
274+
275+
Used for mask fill, `running_max` init, comparisons, and masked-exp zeroing.
276+
No FMA force, no multiwarp gate. WMMA multi-warp stays on.
277+
278+
### 10.5 Verified (R9700, default multiwarp ON, turbo4+paged)
279+
280+
| Check | Result |
281+
|---|---|
282+
| math | coherent |
283+
| full plan handoff prompt | coherent (real plan text, not `/`) |
284+
| PP ~200 tok | ~539 t/s |
285+
| PP ~800 tok | ~895 t/s |
286+
| PP ~2k tok | ~988 t/s |
287+
| PP ~4k tok | **~1015 t/s** (matches pre-fix WMMA ~1048; FMA was ~667) |
288+
289+
Perf hit ≈ **0** vs broken WMMA baseline; **not** the FMA crutch.
290+
291+
### 10.6 Follow-ups
292+
293+
- Same `-INFINITY` pattern still exists in `mt_pagedattn_decode.cu` and
294+
scalar `mt_pagedattn.cu` — latent under `-ffast-math`. Apply the same
295+
sentinel when those paths misbehave.
296+
- Prefer a TU-wide ban on IEEE inf in HIP sources built with fast-math,
297+
or drop `-ffinite-math-only` for attention TUs.

0 commit comments

Comments
 (0)