Repository navigation
[Fix] Restore the gfx950 XCD mapping for bf16 dense attention - #1009
Conversation
There was a problem hiding this comment.
Pull request overview
This PR wires up the existing gfx950 “XCD head-slow remap” (XCD swizzle) to the bf16/f16 dense DUALWAVE_SWP flash-attention path so large-sequence throughput doesn’t degrade due to poor L2 residency across XCDs.
Changes:
- Plumbs an
xcd_swizzlebuild flag through the dense dualwave builder and exposes it viaflydsl_flash_attn_func(..., dualwave_swp_xcd_swizzle=...)(auto/force on/off). - Adds auto-dispatch logic for enabling the remap on eligible dense shapes (mirroring the existing fp8 behavior).
- Adds gfx950 tests to assert bit-identical output when swizzle is toggled and to validate correctness when heads don’t divide the XCD count.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/kernels/test_flash_attn_fwd.py | Adds tests for bit-identical output and correctness around the XCD swizzle option. |
| kernels/attention/flash_attn_interface.py | Exposes dualwave_swp_xcd_swizzle, computes auto-dispatch condition, and passes the flag into the cached dualwave builder. |
| kernels/attention/flash_attn_gfx950.py | Adds a private _xcd_swizzle build parameter and forwards it into traits creation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| outs = [ | ||
| flydsl_flash_attn_func(q, k, v, causal=False, dualwave_swp_xcd_swizzle=flag).clone() for flag in (False, True) | ||
| ] |
There was a problem hiding this comment.
The single-line form was Black's own output at line-length 120 — it collapsed the multi-line comprehension I originally wrote, and black --check passed on it. So the formatting gate was not at risk.
The readability point stands, though. Replaced the comprehension with an explicit local run(flag) helper, which reads better and is stable under Black.
| """H % 8 != 0 must fall back rather than mis-map. | ||
|
|
||
| The remap divides the linear workgroup id by the q-block count to recover | ||
| the head, which only lands each head on one XCD when the head count divides | ||
| evenly into the 8 XCDs. The guard is in the dispatch condition, so this | ||
| checks the kernel still produces a correct answer for such a shape. | ||
| """ |
There was a problem hiding this comment.
Good catch — the docstring was narrower than the actual guarantee. There are two guards: the dispatch condition auto-selects against H % NUM_XCD_GFX950 != 0, and _init_dualwave_thread_mapping re-checks it independently, which is what makes forcing the flag safe. Docstring now states both.
6c5dd25 to
3d0e4b9
Compare
6162271 to
db4143c
Compare
9967add to
43081ba
Compare
43081ba to
6f825c4
Compare
|
LGTM |
#919 made `_init_dualwave_thread_mapping` re-derive (head, q_block) from the linear workgroup id with the head as the slow axis, so a head's blocks stay on one XCD and its K/V stays resident in that XCD's L2 across the concurrent wavefronts. It applied to every qualifying dualwave dense shape, bf16 included, gated only on `not SPLITK and not CAUSAL and NUM_HEADS_Q % NUM_XCD_GFX950 == 0`. #950 put a trait in front of that condition: - if const_expr(not traits.SPLITK and not traits.CAUSAL and ...): + if const_expr(traits.XCD_SWIZZLE and not traits.SPLITK and ...): `XCD_SWIZZLE` defaults to False and only the fp8 builder passes it. That PR measured and described fp8 only, and never touched the bf16 builder -- the string `xcd` does not appear in flash_attn_gfx950.py at all -- so the remap silently stopped applying to bf16. No test covered the mapping, so nothing caught it. Bisecting upstream on MI355X, B=2 H=32 D=128 bf16 non-causal, cache cold, TFLOP/s at S = 61,380 / 180,180 / 239,580, aiter in the same process as control: cf1db2f 7/31 #919 1284.6 1288.2 1286.8 772f070 8/6 #974 1256.7 1188.1 1184.3 494b2cb 8/9 #945 1255.6 1187.9 1187.2 f6d0a41 8/14 #991 1256.0 1182.8 1183.6 47009a3 8/14 #1004 1260.5 1188.6 1183.4 daee90e 8/17 #930 1254.1 1077.0 1060.9 96af91b 8/17 #1014 1251.8 1074.9 1060.6 4521c8c 8/18 #960 1249.3 1073.9 1059.0 aiter ~1252 ~1255 ~1255 Two independent steps. The first, between 7/31 and 8/6 (-8.0%), is #950 and is what this commit fixes. The second, between 47009a3 and daee90e (-10.4%), is #930 dropping the fastmath flag when it moved these kernels onto fx.maxnumf, which reaches the raw MLIR builder without passing through resolve_fastmath; that is a separate fix, filed separately. This plumbs `xcd_swizzle` through the bf16 builder and reuses the existing dispatch condition (non-causal, `H % NUM_XCD_GFX950 == 0`, `num_q_blocks >= MIN_Q_BLOCKS_XCD_SWIZZLE`). Split-K is excluded structurally: it takes the `_build_splitk` branch above this dispatch site. The remap is bijective, so output is bit-identical. Two tests assert that over H=8/16/32/64 and cover the `H % 8 != 0` fallback both forced and auto -- the guard #919 shipped without. One loose end: the restored figure is 1268.3 against cf1db2f's 1286.8, a 1.4% gap. That is #930's dropped fastmath, which costs 10.4% with the remap off but only 1.4% with it on -- the two interact. Fixing #930 separately closes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6f825c4 to
0e41bfb
Compare
|
Post-merge follow-up from a three-pass review: I found no correctness blocker in the landed code. The remap is bijective over the launched grid, the ineligible guards are preserved, and Two durable follow-ups would prevent this feature from silently disappearing again:
These are test/documentation follow-ups, not a request to revert the merged fix. |
Summary
Restore the XCD workgroup remap on the gfx950 bf16 dense
DUALWAVE_SWPpath. It shipped in #919 and applied to bf16 automatically; #950 put a default-off trait in front of it while optimising fp8, and bf16 has run without it since 8/3. The remap is bijective, so this is a timing change with bit-identical output.#919 gated the remap on shape alone. #950 added a trait:
XCD_SWIZZLEdefaults toFalseand only the fp8 builder passes it. That PR was scoped to fp8 and never touched the bf16 builder —xcddoes not appear inflash_attn_gfx950.pyat all — and no test covered the mapping, so nothing failed.Changes
xcd_swizzlethrough the bf16 builder and reuse the existing dispatch condition (non-causal,H % NUM_XCD_GFX950 == 0,num_q_blocks >= MIN_Q_BLOCKS_XCD_SWIZZLE). Auto-dispatched, withdualwave_swp_xcd_swizzle=True/Falseto force. Split-K takes the_build_splitkbranch above this site and is excluded structurally.block_mliteral toDUALWAVE_SWP_BLOCK_Mso the dispatch can count q-blocks before traits exist.H % 8 != 0fallback.Result
Same configuration, median of three, cache cold. "Before" forces the remap off, which is what
maindoes today.Long sequences go from 15.0% behind aiter to 1.4% ahead, and land 1.4% short of the 1286.8 measured at #919's merge — that remainder is #930 dropping a fastmath flag, fixed separately in #1035. Short ones barely move: the decay only bites once a head's K/V outgrows the XCD's L2 slice, the mechanism #919 documented (L2 hit 2.36% → 93.62% at S=65536 H=64).
Testing
The cache flag is not optional here: this change touches helper code outside the traced closure, so the JIT cache key does not move with it and a warm cache serves the previous kernel —
docs/kernel_authoring_guide.mdcalls this case out.@_requires_gfx950.Breaking Changes
None. The remap is bijective and the tests assert bit-identical output.
cc @binding7012 @coderfeli — #950 is where the default was introduced; flagging so the fp8 side can confirm nothing there depends on bf16 staying off.