Skip to content

Commit 5835c73

Browse files
johnnynunezdrisspg
andauthored
[NVIDIA][CuTe,Fwd,sm120] Implement Pack-GQA on SM120 (+ graceful SplitKV fallback) (#2656)
* [CuTe,Fwd,sm120] Fix use_tma_O crash on SM120 (issue #2649) On SM120 (Blackwell GeForce / RTX PRO 6000 / DGX Spark) the forward kernel set `use_tma_O = self.arch >= Arch.sm_90`, enabling the TMA-based O-store epilogue. But SM120 does not build the TMA store atom (tma_atom_O is None), so any forward call crashes in cpasync.tma_partition with: AttributeError: 'NoneType' object has no attribute '_trait' This makes the CuTe-DSL forward unusable on every SM120 GPU. Restrict the TMA O-store to sm_90..sm_119, which is where the WGMMA-era epilogue path is actually available: self.use_tma_O = Arch.sm_90 <= self.arch < Arch.sm_120 SM120 falls back to the non-TMA register->gmem O store (already used for the SM80 path), which is correct and what the CpAsync SM120 kernel expects. Verified on RTX PRO 6000 Blackwell (sm_120, cc 12.0), torch 2.12.0+cu130, nvidia-cutlass-dsl 4.5.2: forward now runs and matches PyTorch SDPA reference for hdim 64/96/128, causal and non-causal (max abs err <= 8e-3 in bf16). Before this fix every SM120 forward call raised the AttributeError above. * [CuTe,Fwd,sm120] Implement Pack-GQA on SM120; graceful SplitKV fallback Pack-GQA was only half-wired in the SM80/SM120 CpAsync forward: the epilogue referenced PackGQA.store_O/store_LSE, but the Q-load and head-indexing used the plain (unpacked) path. So pack_gqa=True crashed in pack_gqa.store_O (crd2idx on a packed (h_idx, m_idx) coordinate against an unpacked mO layout). This implements Pack-GQA end to end on SM120 (and SM80), mirroring the SM90 path: - Reshape mQ/mO (head_idx=2) and mLSE (head_idx=1) via pack_gqa_layout so qhead_per_kvhead folds into the seqlen mode ((qhead, seqlen)). - Scheduler args use cute.size(mQ.shape[0]) (packed total rows) and seqlen_q_static = mQ.shape[0][1] (logical seqlen), so causal/mask q_idx stay correct. - Kernel head-indexing: when pack_gqa, num_head from the scheduler already indexes the KV head (mQ/mK share nheads_kv); no division. - Q-load: gather rows via PackGQA.load_Q (per-row (h_idx, m_idx) gmem pointers) instead of the contiguous local_tile path. SplitKV (num_splits>1) is an SM100-only feature (SM80/SM90 also assert it unsupported); SM120 has no forward+combine path. Fall back to num_splits=1, which is numerically correct, instead of crashing in _check_type on the fp32 partials. Verified on RTX PRO 6000 Blackwell (sm_120): pack_gqa=True matches PyTorch SDPA GQA/MQA reference (err <= 8.4e-3 bf16) AND is bit-identical to the unpacked path (max |packed - unpacked| = 0.0) across MHA/GQA/MQA, causal/non-causal, hd 64/128, seqlen 512-2048. num_splits=3 falls back and matches reference (err 6.8e-4). Stacked on the SM120 use_tma_O fix (#2649). * re-enable SM120 pack-gqa after rebase * clean up SM120 pack-gqa split handling * fix SM120 varlen pack-gqa offset --------- Co-authored-by: drisspg <drisspguessous@gmail.com>
1 parent 6e646e0 commit 5835c73

4 files changed

Lines changed: 46 additions & 27 deletions

File tree

flash_attn/cute/flash_fwd.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from flash_attn.cute.softmax import Softmax, apply_score_mod_inner
3131
from flash_attn.cute.seqlen_info import SeqlenInfoQK
3232
from flash_attn.cute.block_info import BlockInfo
33-
from flash_attn.cute.pack_gqa import PackGQA
33+
from flash_attn.cute.pack_gqa import PackGQA, pack_gqa_layout
3434
from flash_attn.cute.named_barrier import NamedBarrierFwd
3535
from flash_attn.cute.block_sparsity import BlockSparseTensors
3636
from flash_attn.cute.tile_scheduler import SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
@@ -655,8 +655,7 @@ def __call__(
655655
self.num_producer_threads = self.num_threads
656656
self.num_Q_load_threads = self.num_threads
657657
self.num_epilogue_threads = self.num_threads
658-
# self.use_tma_O = self.arch >= 90 and mCuSeqlensQ is None
659-
self.use_tma_O = self.arch >= Arch.sm_90
658+
self.use_tma_O = Arch.sm_90 <= self.arch < Arch.sm_120
660659
self._setup_attributes()
661660
SharedStorage = self._get_shared_storage_cls()
662661
mQ, mK, mV, mO = [assume_tensor_aligned(t) for t in (mQ, mK, mV, mO)]
@@ -674,6 +673,12 @@ def __call__(
674673
if const_expr(mLSE is not None):
675674
LSE_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
676675
mLSE = cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose))
676+
if const_expr(self.pack_gqa):
677+
nheads_kv = mK.shape[2]
678+
mQ = pack_gqa_layout(mQ, self.qhead_per_kvhead, nheads_kv, head_idx=2)
679+
mO = pack_gqa_layout(mO, self.qhead_per_kvhead, nheads_kv, head_idx=2)
680+
if const_expr(mLSE is not None):
681+
mLSE = pack_gqa_layout(mLSE, self.qhead_per_kvhead, nheads_kv, head_idx=1)
677682
# TileScheduler for varlen, simple grid for non-varlen
678683
if const_expr(mCuSeqlensQ is not None or mSeqUsedQ is not None):
679684
TileScheduler = SingleTileVarlenScheduler
@@ -682,10 +687,10 @@ def __call__(
682687
num_batch = (
683688
mCuSeqlensQ.shape[0] - 1
684689
if const_expr(mCuSeqlensQ is not None)
685-
else mQ.shape[3]
690+
else cute.size(mQ.shape[3])
686691
)
687692
tile_sched_args = TileSchedulerArguments(
688-
num_block=cute.ceil_div(mQ.shape[0], self.tile_m),
693+
num_block=cute.ceil_div(cute.size(mQ.shape[0]), self.tile_m),
689694
num_head=cute.size(mQ.shape[2]),
690695
num_batch=num_batch,
691696
num_splits=1,
@@ -794,7 +799,7 @@ def kernel(
794799
)
795800
seqlen = SeqlenInfoQK.create(
796801
batch_idx=batch_size,
797-
seqlen_q_static=mQ.shape[0],
802+
seqlen_q_static=mQ.shape[0] if const_expr(not self.pack_gqa) else mQ.shape[0][1],
798803
seqlen_k_static=mK.shape[0],
799804
mCuSeqlensQ=mCuSeqlensQ,
800805
mCuSeqlensK=mCuSeqlensK,
@@ -814,18 +819,16 @@ def kernel(
814819
blkQ_shape = (self.tile_m, self.tile_hdim)
815820
blkK_shape = (self.tile_n, self.tile_hdim)
816821
blkV_shape = (self.tile_n, self.tile_hdimv)
817-
num_head_kv = num_head // self.qhead_per_kvhead
818-
if const_expr(not seqlen.has_cu_seqlens_q):
819-
mQ_cur = mQ[None, None, num_head, batch_size]
820-
else:
821-
mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, None, num_head])
822+
num_head_kv = num_head if const_expr(self.pack_gqa) else num_head // self.qhead_per_kvhead
823+
mQ_cur = seqlen.offset_batch_Q(mQ, batch_size, dim=3)[None, None, num_head]
822824
if const_expr(not seqlen.has_cu_seqlens_k):
823825
mK_cur = mK[None, None, num_head_kv, batch_size]
824826
mV_cur = mV[None, None, num_head_kv, batch_size]
825827
else:
826828
mK_cur = cute.domain_offset((seqlen.offset_k, 0), mK[None, None, num_head_kv])
827829
mV_cur = cute.domain_offset((seqlen.offset_k, 0), mV[None, None, num_head_kv])
828-
gQ = cute.local_tile(mQ_cur, blkQ_shape, (m_block, 0))
830+
if const_expr(not self.pack_gqa):
831+
gQ = cute.local_tile(mQ_cur, blkQ_shape, (m_block, 0))
829832
gK = cute.local_tile(mK_cur, blkK_shape, (None, 0))
830833
gV = cute.local_tile(mV_cur, blkV_shape, (None, 0))
831834

@@ -957,7 +960,11 @@ def kernel(
957960
# ///////////////////////////////////////////////////////////////////////////////
958961
# Start async loads of the last mn-tile, where we take care of the mn residue
959962
gmem_thr_copy_Q = gmem_tiled_copy_Q.get_slice(tidx)
960-
self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q, headdim=mQ.shape[1])
963+
if const_expr(not self.pack_gqa):
964+
self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q, headdim=mQ.shape[1])
965+
else:
966+
pack_gqa = PackGQA(self.tile_m, self.tile_hdim, self.check_hdim_oob, self.qhead_per_kvhead)
967+
pack_gqa.load_Q(mQ_cur, sQ, gmem_tiled_copy_Q, tidx, m_block, seqlen.seqlen_q)
961968
cute.arch.cp_async_commit_group()
962969

963970
def preprocess_Q():

flash_attn/cute/interface.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,8 +459,6 @@ def _flash_attn_fwd(
459459
qhead_per_kvhead = num_head // num_head_kv
460460
if pack_gqa is None:
461461
pack_gqa = qhead_per_kvhead > 1
462-
if arch // 10 == 12:
463-
pack_gqa = False
464462

465463
is_fp8 = v.dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
466464
requires_grad = any(t is not None and t.requires_grad for t in [q, k, v, qv])
@@ -566,7 +564,9 @@ def _flash_attn_fwd(
566564
total_mblocks = batch_size * num_head_kv * num_m_blocks
567565
num_n_blocks = (seqlen_k_loaded + tile_n - 1) // tile_n
568566
num_SMs = 132 if is_fake_mode() else torch.cuda.get_device_properties(device).multi_processor_count
569-
if num_splits < 1:
567+
if arch // 10 == 12:
568+
assert num_splits == 1, "SM120 forward only supports num_splits=1"
569+
elif num_splits < 1:
570570
num_splits = num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, 128)
571571

572572
# SplitKV uses float32 partial output, which doubles the O buffer size

tests/cute/test_flash_attn.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,23 @@ def check_tensor_vs_ref(name, actual, ref, pt, rtol=2, atol=None):
7979
# When operating fake tensors, we cannot perform data-dependent operations (e.g., `tensor.max()`).
8080
USE_FAKE_TENSOR = int(os.getenv("FLASH_ATTENTION_FAKE_TENSOR", 0)) == 1
8181
DISABLE_SPLIT = os.getenv("FLASH_ATTENTION_DISABLE_SPLIT", "FALSE") == "TRUE"
82-
# SplitKV is not supported on SM90
82+
# SplitKV is not supported on SM90 or SM120
8383
IS_SM90 = torch.cuda.get_device_capability()[0] == 9
8484
IS_SM100 = torch.cuda.get_device_capability()[0] == 10
85+
IS_SM120 = torch.cuda.get_device_capability()[0] == 12
8586
TEST_BWD_ONLY = False
8687
VERBOSE = True
8788

89+
90+
@pytest.mark.skipif(not IS_SM120, reason="SM120-only SplitKV unsupported behavior")
91+
def test_flash_attn_sm120_rejects_splitkv():
92+
q = torch.randn(1, 16, 4, 64, device="cuda", dtype=torch.bfloat16)
93+
k = torch.randn(1, 16, 1, 64, device="cuda", dtype=torch.bfloat16)
94+
v = torch.randn(1, 16, 1, 64, device="cuda", dtype=torch.bfloat16)
95+
with pytest.raises(AssertionError, match="SM120 forward only supports num_splits=1"):
96+
flash_attn_func(q, k, v, num_splits=3)
97+
98+
8899
# @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float8_e4m3fn])
89100
@pytest.mark.parametrize("dtype", [torch.bfloat16])
90101
@pytest.mark.parametrize("mha_type", ["mha", "mqa", "gqa"])
@@ -321,8 +332,8 @@ def test_flash_attn_output(
321332
# pack_gqa_vals = [False]
322333
num_splits_vals = [1, 3] if d < 192 and not DISABLE_SPLIT and not TEST_BWD_ONLY and not has_qv else [1]
323334
for pack_gqa, num_splits in itertools.product(pack_gqa_vals, num_splits_vals):
324-
# SplitKV not supported on SM90 - skip this iteration
325-
if IS_SM90 and num_splits > 1:
335+
# SplitKV not supported on SM90/SM120 - skip this iteration
336+
if (IS_SM90 or IS_SM120) and num_splits > 1:
326337
continue
327338
if IS_SM100 and (d >= 192 and dv >= 192) and not (d == 256 and dv == 256):
328339
continue
@@ -854,8 +865,8 @@ def _gen_unused_masks(padding_mask, add_unused, max_seq_len, bs, device):
854865
# SplitKV is not supported for hdim >= 192
855866
num_splits_vals = [1, 3] if d < 192 and not DISABLE_SPLIT and not TEST_BWD_ONLY else [1]
856867
for pack_gqa, num_splits in itertools.product(pack_gqa_vals, num_splits_vals):
857-
# SplitKV not supported on SM90 - skip this iteration
858-
if IS_SM90 and num_splits > 1:
868+
# SplitKV not supported on SM90/SM120 - skip this iteration
869+
if (IS_SM90 or IS_SM120) and num_splits > 1:
859870
continue
860871
# TODO(wangsiyu): SM100 head_dim=256 2CTA kernel does not support pack_gqa yet.
861872
# pack_gqa=None means auto-enable for GQA/MQA (qhead_per_kvhead > 1)
@@ -1477,8 +1488,8 @@ def test_flash_attn_kvcache(
14771488
for num_splits, precompute_metadata in itertools.product(
14781489
num_splits_vals, precompute_metadata_vals
14791490
):
1480-
# SplitKV not supported on SM90 - skip this iteration
1481-
if IS_SM90 and num_splits > 1:
1491+
# SplitKV not supported on SM90/SM120 - skip this iteration
1492+
if (IS_SM90 or IS_SM120) and num_splits > 1:
14821493
continue
14831494
# if precompute_metadata:
14841495
# scheduler_metadata = get_scheduler_metadata(
@@ -2559,8 +2570,8 @@ def _gen_unused_masks(padding_mask, add_unused, max_seq_len, bs, device):
25592570
pack_gqa_vals = [True]
25602571
num_splits_vals = [1]
25612572
for pack_gqa, num_splits in itertools.product(pack_gqa_vals, num_splits_vals):
2562-
# SplitKV not supported on SM90 - skip this iteration
2563-
if IS_SM90 and num_splits > 1:
2573+
# SplitKV not supported on SM90/SM120 - skip this iteration
2574+
if (IS_SM90 or IS_SM120) and num_splits > 1:
25642575
continue
25652576
out_unpad, lse = flash_attn_varlen_func(
25662577
q_unpad if unpad_q else q,

tests/cute/test_flash_attn_fast.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
USE_FAKE_TENSOR = int(os.getenv("FLASH_ATTENTION_FAKE_TENSOR", 0)) == 1
2727
IS_SM90 = torch.cuda.get_device_capability()[0] == 9
28+
IS_SM120 = torch.cuda.get_device_capability()[0] == 12
2829

2930

3031
# ---------------------------------------------------------------------------
@@ -47,8 +48,8 @@
4748
)
4849
@maybe_fake_tensor_mode(USE_FAKE_TENSOR)
4950
def test_flash_attn_output(seqlen_q, seqlen_k, d, causal, num_splits, mha_type, dtype):
50-
if IS_SM90 and num_splits > 1:
51-
pytest.skip("SM90 fwd doens't support num_splits > 1")
51+
if (IS_SM90 or IS_SM120) and num_splits > 1:
52+
pytest.skip("SM90/SM120 fwd doesn't support num_splits > 1")
5253
device = "cuda"
5354
torch.random.manual_seed(0)
5455
random.seed(0)

0 commit comments

Comments
 (0)