Skip to content

Commit 6c5dd25

Browse files
JohnQinAMDclaude
andcommitted
Enable the XCD head-slow remap on the bf16 dense attention path
The DUALWAVE_SWP dense kernel launches grid=(NUM_HEADS_Q, num_q_blocks, grid_z), which is head-fast: one head's q-blocks scatter across all 8 XCDs and every XCD re-streams that head's K/V from HBM. With 256 CUs at one workgroup/CU the number of concurrent distinct K/V streams is the head count, so at H=32/64 the working set outgrows L2 and throughput decays as S grows. Measured on MI355X, bf16, D=128, non-causal, B=2 H=32: the L2 hit rate falls from 86% at S=27k to 37% at S=180k, where the kernel reads 1182 GB from HBM against a compulsory 8.9 GB -- 133x -- while aiter holds 87% and 28x over the same range. The fix already existed. _init_dualwave_thread_mapping re-derives (head, q_block) with head as the slow axis, and _make_dualwave_swp_traits already accepted xcd_swizzle; only the fp8 builder ever passed it, gated on dtype_str == "fp8". This plumbs the flag through the bf16 builder and reuses the fp8 dispatch condition (non-causal, H % NUM_XCD_GFX950 == 0, num_q_blocks >= MIN_Q_BLOCKS_XCD_SWIZZLE, no split-K). With the remap on, FlyDSL's memory profile lands on aiter's: 89.2% hit and 28.1x re-stream at S=180k, against aiter's 87.1% and 28.2x. Throughput goes flat at 1264-1272 TFLOP/s (84.5% of this box's 1497 TFLOP/s bf16 GEMM ceiling) across S=27k..240k, 2.7-3.1% above aiter at every point. Best case is +8.5% at S=239,580 H=64. The remap is bijective, so the output is bit-identical to the unpatched kernel -- pinned by a new test. H=8 and H=16 already fit L2 and show no decay; the remap measures as a no-op there (0.999-1.000x), so the dispatch does not gate on head count. dualwave_swp_xcd_swizzle joins the existing dualwave_swp_* build options: None auto-selects, True/False force it, which is what lets the test drive both paths through the public API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: John Qin <yanyuan.qin@amd.com>
1 parent ac227c3 commit 6c5dd25

3 files changed

Lines changed: 75 additions & 1 deletion

File tree

kernels/attention/flash_attn_gfx950.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def build_flash_attn_dualwave_swp_module(
6767
paged=False,
6868
kv_cache_layout="linear",
6969
return_lse=False,
70+
_xcd_swizzle=False,
7071
):
7172
"""Build an DUALWAVE_SWP flash_attn launcher for D=64/128 bf16/f16 on gfx950.
7273
@@ -118,6 +119,7 @@ def build_flash_attn_dualwave_swp_module(
118119
kv_cache_layout=kv_cache_layout,
119120
kv_vectorized=KV_VECTORIZED,
120121
return_lse=return_lse,
122+
xcd_swizzle=_xcd_swizzle,
121123
)
122124
traits.BLOCK_N_OUT // traits.BLOCK_N
123125
_dualwave_swp_cache_tag = traits.cache_tag

kernels/attention/flash_attn_interface.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
import torch.nn.functional as F # noqa: F401 (imported for callers' convenience)
3030

3131
# Re-export so callers only need to import from this module.
32-
from kernels.attention.flash_attn_utils import dualwave_splitk_workspace_elems
32+
from kernels.attention.flash_attn_utils import (
33+
MIN_Q_BLOCKS_XCD_SWIZZLE,
34+
NUM_XCD_GFX950,
35+
dualwave_splitk_workspace_elems,
36+
)
3337

3438
__all__ = ["flydsl_flash_attn_func", "dualwave_splitk_workspace_elems"]
3539

@@ -133,6 +137,7 @@ def _build_dense_dualwave(
133137
debug_lazy_counts: bool,
134138
enable_stagger: bool,
135139
return_lse: bool = False,
140+
xcd_swizzle: bool = False,
136141
):
137142
"""Build (and cache) the dense gfx950 DUALWAVE_SWP launcher."""
138143
from kernels.attention.flash_attn_gfx950 import build_flash_attn_dualwave_swp_module
@@ -151,6 +156,7 @@ def _build_dense_dualwave(
151156
dualwave_swp_debug_lazy_counts=debug_lazy_counts,
152157
dualwave_swp_enable_stagger=enable_stagger,
153158
return_lse=return_lse,
159+
_xcd_swizzle=xcd_swizzle,
154160
)
155161

156162

@@ -625,6 +631,10 @@ def flydsl_flash_attn_func(
625631
dualwave_swp_lazy_rescale: bool = True,
626632
dualwave_swp_setprio: bool = True,
627633
dualwave_swp_enable_stagger: bool = True,
634+
# Re-derive (head, q_block) with head as the slow axis so one head's q-blocks
635+
# stay on one XCD instead of every XCD re-streaming that head's K/V. None
636+
# auto-selects on the shapes it helps; True/False force it. Dense non-fp8 only.
637+
dualwave_swp_xcd_swizzle: Optional[bool] = None,
628638
# Debug: pass a pre-allocated float32[2] tensor to enable the lazy-rescale
629639
# branch counter (dualwave_swp_debug_lazy_counts=True). Only for dense mode.
630640
debug_counts: Optional[torch.Tensor] = None,
@@ -873,6 +883,20 @@ def flydsl_flash_attn_func(
873883
"flydsl_flash_attn_func: debug_counts requires the gfx950 DUALWAVE_SWP path"
874884
)
875885
if debug_lazy or (can_dualwave and _dense_routes_to_dualwave(B, Sq)):
886+
# Head-fast grid scatters one head's q-blocks across all XCDs, so
887+
# every XCD re-streams that head's K/V and L2 residency collapses
888+
# as S grows (measured: 86% hit at S=27k down to 37% at S=180k,
889+
# 133x the compulsory HBM read). The head-slow remap in
890+
# _init_dualwave_thread_mapping fixes it and is bijective, so the
891+
# output is bit-identical. The fp8 path already dispatches this
892+
# way; bf16 never plumbed the flag through.
893+
num_q_blocks = (int(Sq) + 255) // 256
894+
if dualwave_swp_xcd_swizzle is None:
895+
xcd_swizzle = (
896+
not causal and H % NUM_XCD_GFX950 == 0 and num_q_blocks >= MIN_Q_BLOCKS_XCD_SWIZZLE
897+
)
898+
else:
899+
xcd_swizzle = dualwave_swp_xcd_swizzle
876900
exe = _build_dense_dualwave(
877901
num_heads=H,
878902
num_kv_heads=num_kv_heads,
@@ -887,6 +911,7 @@ def flydsl_flash_attn_func(
887911
debug_lazy_counts=debug_lazy,
888912
enable_stagger=dualwave_swp_enable_stagger,
889913
return_lse=return_lse,
914+
xcd_swizzle=xcd_swizzle,
890915
)
891916
else:
892917
block_m, flat_work_group_size, path_tag = _dense_generic_tile(B, Sq, H, D, dtype_str, q.device)

tests/kernels/test_flash_attn_fwd.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3471,5 +3471,52 @@ def test_return_lse_rejects_fp8():
34713471
)
34723472

34733473

3474+
@_requires_gfx950
3475+
@pytest.mark.parametrize("H", [8, 16, 32, 64])
3476+
def test_xcd_swizzle_is_bit_identical(H):
3477+
"""The head-slow remap must not change a single bit of the output.
3478+
3479+
It only re-derives (head, q_block) from the same linear workgroup id, so it
3480+
is bijective by construction -- but a mistake in the derivation would show
3481+
up as a permuted or partially-recomputed output rather than as an error, so
3482+
this pins it. S clears the auto-dispatch threshold (num_q_blocks >= 64 at
3483+
BLOCK_M=256) so both settings run on the shapes the remap targets.
3484+
"""
3485+
S = 64 * 256
3486+
dtype = torch.bfloat16
3487+
torch.manual_seed(H)
3488+
q = _rand_lse(1, S, H, 128, dtype=dtype)
3489+
k, v = torch.randn_like(q), torch.randn_like(q)
3490+
3491+
outs = [
3492+
flydsl_flash_attn_func(q, k, v, causal=False, dualwave_swp_xcd_swizzle=flag).clone() for flag in (False, True)
3493+
]
3494+
torch.cuda.synchronize()
3495+
assert torch.equal(outs[0], outs[1])
3496+
3497+
3498+
@_requires_gfx950
3499+
def test_xcd_swizzle_skipped_when_heads_not_multiple_of_xcd():
3500+
"""H % 8 != 0 must fall back rather than mis-map.
3501+
3502+
The remap divides the linear workgroup id by the q-block count to recover
3503+
the head, which only lands each head on one XCD when the head count divides
3504+
evenly into the 8 XCDs. The guard is in the dispatch condition, so this
3505+
checks the kernel still produces a correct answer for such a shape.
3506+
"""
3507+
S, H = 64 * 256, 12
3508+
dtype = torch.bfloat16
3509+
torch.manual_seed(H)
3510+
q = _rand_lse(1, S, H, 128, dtype=dtype)
3511+
k, v = torch.randn_like(q), torch.randn_like(q)
3512+
3513+
out = flydsl_flash_attn_func(q, k, v, causal=False)
3514+
torch.cuda.synchronize()
3515+
ref = F.scaled_dot_product_attention(
3516+
q.transpose(1, 2).float(), k.transpose(1, 2).float(), v.transpose(1, 2).float()
3517+
).transpose(1, 2)
3518+
torch.testing.assert_close(out.float(), ref, atol=_ATOL_BF16, rtol=0)
3519+
3520+
34743521
if __name__ == "__main__":
34753522
main()

0 commit comments

Comments
 (0)