[ROCm][DSV4][Perf] Optimize Triton sparse-MLA decode on gfx950 (#52212)
Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com> Signed-off-by: fai <fangzhouai@gmail.com> Co-authored-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Cursor Grok 4.6 <cursoragent@cursor.com>
Fangzhou Ai committed
Aug 16, 2026 at 15:16 UTC
ef43e3101b8fda8cd2b52de150c76b4fc177fad2
5 files changed
+1550
-94
tests/kernels/attention/test_rocm_triton_attn_dsv4.py
+458
-23
@@ -24,12 +24,27 @@ def _on_split_decode_arch() -> bool:
24
return False
25
26
27
+def _on_gfx950() -> bool:
28
+ if not current_platform.is_rocm():
29
+ return False
30
+ try:
31
+ from vllm.platforms.rocm import _ON_GFX950
32
+
33
+ return _ON_GFX950
34
+ except ImportError:
35
+ return False
36
+
37
+
38
# The flash-decode split-K decode path is only tuned for AMD gfx942/gfx950; other
39
# architectures take the fallback decode kernel, so its tests are skipped there.
40
requires_split_decode_arch = pytest.mark.skipif(
41
not _on_split_decode_arch(),
42
reason="split-K decode kernel is only tuned for AMD gfx942/gfx950",
43
)
44
+requires_gfx950 = pytest.mark.skipif(
45
+ not _on_gfx950(),
46
+ reason="optimized sparse decode partial is gfx950-only",
47
+)
48
49
NOPE_HEAD_DIM = 448
50
ROPE_HEAD_DIM = 64
@@ -118,28 +133,48 @@ def _pack_fp8_ds_mla_cache(
133
return cache
134
135
121
-def _read_fp8_ds_mla_cache(
122
- cache: torch.Tensor, slot: int, block_size: int, use_fnuz: bool
123
-) -> torch.Tensor:
124
- cache_flat = cache.view(torch.uint8).flatten()
136
+def _poison_fp8_ds_mla_cache_row(
137
+ cache: torch.Tensor, block_size: int, slot: int = 0
138
+) -> None:
139
+ flat = cache.flatten()
140
block_idx = slot // block_size
141
pos = slot % block_size
142
block_base = block_idx * cache.stride(0)
143
token_base = block_base + pos * 576
144
scale_base = block_base + block_size * 576 + pos * 8
145
+ flat[token_base] = 0x7F
146
+ flat[scale_base : scale_base + 7] = 255
147
+ flat[token_base + NOPE_HEAD_DIM : token_base + 576].view(torch.bfloat16)[0] = float(
148
+ "nan"
149
+ )
150
+
151
+
152
+def _read_fp8_ds_mla_cache_rows(
153
+ cache: torch.Tensor,
154
+ slots: torch.Tensor,
155
+ block_size: int,
156
+ use_fnuz: bool,
157
+) -> torch.Tensor:
158
+ cache_flat = cache.view(torch.uint8).flatten()
159
+ block_idx = slots // block_size
160
+ pos = slots % block_size
161
+ block_base = block_idx * cache.stride(0)
162
+ token_base = block_base + pos * 576
163
+ scale_base = block_base + block_size * 576 + pos * 8
164
165
fp8_dtype = torch.float8_e4m3fnuz if use_fnuz else torch.float8_e4m3fn
132
- nope_u8 = cache_flat[token_base : token_base + NOPE_HEAD_DIM]
166
+ nope_offsets = torch.arange(NOPE_HEAD_DIM, device=cache.device)
167
+ nope_u8 = cache_flat[token_base[:, None] + nope_offsets]
168
nope = nope_u8.view(fp8_dtype).to(torch.float32)
169
+ scale_offsets = torch.arange(7, device=cache.device)
170
scales = torch.exp2(
135
- cache_flat[scale_base : scale_base + 7].to(torch.float32) - 127.0
171
+ cache_flat[scale_base[:, None] + scale_offsets].to(torch.float32) - 127.0
172
)
137
- nope = nope * scales.repeat_interleave(64)
138
- rope_u8 = cache_flat[
139
- token_base + NOPE_HEAD_DIM : token_base + NOPE_HEAD_DIM + ROPE_HEAD_DIM * 2
140
- ]
141
- rope = rope_u8.view(torch.bfloat16).to(torch.float32)
142
- return torch.cat([nope, rope])
173
+ nope = nope * scales.repeat_interleave(64, dim=1)
174
+ rope_offsets = torch.arange(ROPE_HEAD_DIM * 2, device=cache.device)
175
+ rope_u8 = cache_flat[token_base[:, None] + NOPE_HEAD_DIM + rope_offsets]
176
+ rope = rope_u8.contiguous().view(torch.bfloat16).to(torch.float32)
177
+ return torch.cat([nope, rope], dim=1)
178
179
180
def _ref_sparse_decode_ragged(
@@ -158,19 +193,30 @@ def _ref_sparse_decode_ragged(
193
out = torch.empty_like(q_f32)
194
195
for query_idx in range(q.shape[0]):
161
- row_kv = [
162
- _read_fp8_ds_mla_cache(main_cache, int(slot), block_size, main_use_fnuz)
163
- for slot in main_rows[query_idx]
164
- ]
165
- if extra_cache is not None and extra_rows is not None:
166
- row_kv.extend(
167
- _read_fp8_ds_mla_cache(
168
- extra_cache, int(slot), block_size, extra_use_fnuz
196
+ row_kv = []
197
+ if main_rows[query_idx]:
198
+ main_slots = torch.tensor(
199
+ main_rows[query_idx], dtype=torch.int64, device=q.device
200
+ )
201
+ row_kv.append(
202
+ _read_fp8_ds_mla_cache_rows(
203
+ main_cache, main_slots, block_size, main_use_fnuz
204
+ )
205
+ )
206
+ if extra_cache is not None and extra_rows is not None and extra_rows[query_idx]:
207
+ extra_slots = torch.tensor(
208
+ extra_rows[query_idx], dtype=torch.int64, device=q.device
209
+ )
210
+ row_kv.append(
211
+ _read_fp8_ds_mla_cache_rows(
212
+ extra_cache, extra_slots, block_size, extra_use_fnuz
213
)
170
- for slot in extra_rows[query_idx]
214
)
215
173
- kv = torch.stack(row_kv).to(q.device)
216
+ if not row_kv:
217
+ out[query_idx] = 0
218
+ continue
219
+ kv = torch.cat(row_kv)
220
for head_idx in range(q.shape[1]):
221
scores = torch.mv(kv, q_f32[query_idx, head_idx]) * scale
222
if attn_sink is not None:
@@ -198,6 +244,46 @@ def _ragged_from_rows(
244
)
245
246
247
+def _launch_sparse_decode_reduce(
248
+ part_m: torch.Tensor,
249
+ part_l: torch.Tensor,
250
+ part_acc: torch.Tensor,
251
+ adaptive_splits: bool,
252
+) -> torch.Tensor:
253
+ from vllm.v1.attention.ops import rocm_aiter_mla_sparse as mod
254
+
255
+ num_queries, num_splits, num_heads = part_m.shape
256
+ out = torch.empty(
257
+ (num_queries, num_heads, HEAD_DIM),
258
+ dtype=torch.bfloat16,
259
+ device=part_m.device,
260
+ )
261
+ attn_sink = torch.empty(1, dtype=torch.float32, device=part_m.device)
262
+ mod._sparse_attn_decode_reduce_kernel[(num_queries, num_heads)](
263
+ part_m,
264
+ part_l,
265
+ part_acc,
266
+ attn_sink,
267
+ out,
268
+ out.stride(0),
269
+ out.stride(1),
270
+ part_m.stride(0),
271
+ part_m.stride(1),
272
+ part_acc.stride(0),
273
+ part_acc.stride(1),
274
+ part_acc.stride(2),
275
+ num_heads,
276
+ HAS_ATTN_SINK=False,
277
+ ADAPTIVE_SPLITS=adaptive_splits,
278
+ COMB_DIM=HEAD_DIM,
279
+ BLOCK_H=1,
280
+ NUM_SPLITS=num_splits,
281
+ SPLITS_PAD=1 << (num_splits - 1).bit_length(),
282
+ num_warps=4,
283
+ )
284
+ return out
285
+
286
+
287
@torch.inference_mode()
288
def test_paged_mqa_logits_do_not_contain_nan(monkeypatch) -> None:
289
from vllm._aiter_ops import rocm_aiter_ops
@@ -312,6 +398,19 @@ def test_compute_global_topk_ragged_indices_and_indptr() -> None:
398
torch.testing.assert_close(actual_lens, expected_lens)
399
400
401
+def test_extra_cache_nan_free_provenance_gate(monkeypatch) -> None:
402
+ from vllm.models.deepseek_v4.amd import rocm as mod
403
+
404
+ monkeypatch.setattr(mod, "_ON_GFX950", True)
405
+ assert mod._trust_dsv4_extra_cache_nan_free("fp8_ds_mla", False, True)
406
+ assert not mod._trust_dsv4_extra_cache_nan_free("fp8_ds_mla", True, True)
407
+ assert not mod._trust_dsv4_extra_cache_nan_free("bfloat16", False, True)
408
+ assert not mod._trust_dsv4_extra_cache_nan_free("fp8_ds_mla", False, False)
409
+
410
+ monkeypatch.setattr(mod, "_ON_GFX950", False)
411
+ assert not mod._trust_dsv4_extra_cache_nan_free("fp8_ds_mla", False, True)
412
+
413
+
414
@torch.inference_mode()
415
def test_sparse_attn_prefill_ragged_kernel() -> None:
416
from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
@@ -366,6 +465,7 @@ def test_sparse_attn_decode_ragged_kernel() -> None:
465
attn_sink = torch.tensor([-0.1, 0.0, 0.1], dtype=torch.float32, device=device)
466
scale = HEAD_DIM**-0.5
467
468
+ out = torch.empty_like(q)
469
actual = _rocm_sparse_attn_decode_ragged_triton(
470
q=q,
471
main_cache=main_cache,
@@ -378,6 +478,7 @@ def test_sparse_attn_decode_ragged_kernel() -> None:
478
extra_cache=extra_cache,
479
extra_indices=extra_indices,
480
extra_indptr=extra_indptr,
481
+ out=out,
482
)
483
expected = _ref_sparse_decode_ragged(
484
q=q,
@@ -391,9 +492,111 @@ def test_sparse_attn_decode_ragged_kernel() -> None:
492
main_use_fnuz=main_use_fnuz,
493
)
494
495
+ assert actual.data_ptr() == out.data_ptr()
496
torch.testing.assert_close(actual, expected, atol=2e-2, rtol=2e-2)
497
498
499
+@requires_gfx950
500
+@torch.inference_mode()
501
+def test_sparse_attn_decode_scrubs_untrusted_cache_by_default() -> None:
502
+ from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
503
+ _rocm_sparse_attn_decode_ragged_triton,
504
+ )
505
+
506
+ device = torch.device("cuda")
507
+ block_size = 4
508
+ main_cache = torch.zeros(1, block_size, 584, dtype=torch.uint8, device=device)
509
+ extra_cache = torch.zeros_like(main_cache)
510
+ _poison_fp8_ds_mla_cache_row(main_cache, block_size)
511
+ _poison_fp8_ds_mla_cache_row(extra_cache, block_size)
512
+ indices = torch.zeros(1, dtype=torch.int32, device=device)
513
+ indptr = torch.tensor([0, 1], dtype=torch.int32, device=device)
514
+
515
+ actual = _rocm_sparse_attn_decode_ragged_triton(
516
+ q=torch.ones(1, 1, HEAD_DIM, dtype=torch.bfloat16, device=device),
517
+ main_cache=main_cache,
518
+ main_indices=indices,
519
+ main_indptr=indptr,
520
+ scale=HEAD_DIM**-0.5,
521
+ attn_sink=None,
522
+ nope_head_dim=NOPE_HEAD_DIM,
523
+ rope_head_dim=ROPE_HEAD_DIM,
524
+ extra_cache=extra_cache,
525
+ extra_indices=indices,
526
+ extra_indptr=indptr,
527
+ )
528
+
529
+ assert not torch.isnan(actual).any()
530
+ assert torch.equal(actual, torch.zeros_like(actual))
531
+
532
+
533
+@pytest.mark.parametrize("on_gfx950", [False, True])
534
+@torch.inference_mode()
535
+def test_rocm_ragged_graph_buffer_view_tracks_source_width(
536
+ monkeypatch, on_gfx950: bool
537
+) -> None:
538
+ from vllm.models.deepseek_v4.amd import rocm as rocm_mod
539
+
540
+ monkeypatch.setattr(rocm_mod, "_ON_GFX950", on_gfx950)
541
+
542
+ indices_buffer = torch.full((16,), -1, dtype=torch.int32)
543
+ indptr_buffer = torch.full((3,), -1, dtype=torch.int32)
544
+ first_indices = torch.tensor([3, 5, 7], dtype=torch.int32)
545
+ first_indptr = torch.tensor([0, 1, 3], dtype=torch.int32)
546
+ first_view, first_indptr_view = rocm_mod._copy_ragged_to_graph_buffers(
547
+ first_indices,
548
+ first_indptr,
549
+ indices_buffer,
550
+ indptr_buffer,
551
+ num_rows=2,
552
+ max_entries_per_row=8,
553
+ )
554
+
555
+ second_indices = torch.tensor([1, 2, 3, 4, 5, 6], dtype=torch.int32)
556
+ second_indptr = torch.tensor([0, 2, 6], dtype=torch.int32)
557
+ second_view, second_indptr_view = rocm_mod._copy_ragged_to_graph_buffers(
558
+ second_indices,
559
+ second_indptr,
560
+ indices_buffer,
561
+ indptr_buffer,
562
+ num_rows=2,
563
+ max_entries_per_row=8,
564
+ )
565
+
566
+ expected_first_entries = (
567
+ first_indices.numel() if on_gfx950 else indices_buffer.numel()
568
+ )
569
+ expected_second_entries = (
570
+ second_indices.numel() if on_gfx950 else indices_buffer.numel()
571
+ )
572
+ assert first_view.numel() == expected_first_entries
573
+ assert second_view.numel() == expected_second_entries
574
+ assert first_view.data_ptr() == second_view.data_ptr() == indices_buffer.data_ptr()
575
+ assert first_indptr_view.data_ptr() == second_indptr_view.data_ptr()
576
+ assert torch.equal(second_view[: second_indices.numel()], second_indices)
577
+ assert torch.equal(second_indptr_view, second_indptr)
578
+
579
+
580
+def test_rocm_capture_metadata_sets_adaptive_marker(monkeypatch) -> None:
581
+ from vllm.models.deepseek_v4.amd import rocm as rocm_mod
582
+ from vllm.models.deepseek_v4.sparse_mla import (
583
+ DeepseekV4SparseMLAMetadataBuilder,
584
+ )
585
+
586
+ metadata = SimpleNamespace(for_cudagraph_capture=False)
587
+ monkeypatch.setattr(
588
+ DeepseekV4SparseMLAMetadataBuilder,
589
+ "build_for_cudagraph_capture",
590
+ lambda *_: metadata,
591
+ )
592
+ builder = object.__new__(rocm_mod.DeepseekV4ROCMAiterMLASparseMetadataBuilder)
593
+
594
+ actual = builder.build_for_cudagraph_capture(SimpleNamespace())
595
+
596
+ assert actual is metadata
597
+ assert actual.for_cudagraph_capture is _on_gfx950()
598
+
599
+
600
@requires_split_decode_arch
601
@torch.inference_mode()
602
def test_decode_num_splits_heuristic(monkeypatch) -> None:
@@ -408,6 +611,9 @@ def test_decode_num_splits_heuristic(monkeypatch) -> None:
611
# A tiny batch on a large device should split to add parallelism.
612
assert mod._decode_num_splits(2, 1, avg_main_len=256.0, avg_extra_len=0.0) > 1
613
614
+ # The shared gfx942 selector retains its original 16-split ceiling.
615
+ assert mod._decode_num_splits(1, 1, 128.0, 8192.0) == 16
616
+
617
# The chosen count always stays within the searched [1, 16] range, and a
618
# zero-length workload never splits (no work to parallelize).
619
for num_queries in (1, 4, 24, 224, 1024):
@@ -418,6 +624,16 @@ def test_decode_num_splits_heuristic(monkeypatch) -> None:
624
assert mod._decode_num_splits(2, 1, avg_main_len=0.0, avg_extra_len=0.0) >= 1
625
626
627
+@torch.inference_mode()
628
+def test_decode_num_splits_gfx950(monkeypatch) -> None:
629
+ from vllm.v1.attention.ops import rocm_aiter_mla_sparse as mod
630
+
631
+ monkeypatch.setattr(mod, "_decode_cu_count", lambda: 256)
632
+ assert mod._decode_gfx950_num_splits(1, 1, 128, 8192) == 32
633
+ assert mod._decode_gfx950_num_splits(17, 1, 128, 32) == 4
634
+ assert mod._decode_gfx950_num_splits(512, 1, 128, 7812) == 1
635
+
636
+
637
@requires_split_decode_arch
638
@pytest.mark.parametrize("num_splits", [1, 2, 3, 4, 8])
639
@pytest.mark.parametrize("with_extra", [True, False])
@@ -473,7 +689,14 @@ def test_sparse_attn_decode_split_k_kernel(
689
scale = HEAD_DIM**-0.5
690
691
# Pin the split count so each parametrized value is exercised deterministically.
476
- monkeypatch.setattr(mod, "_decode_num_splits", lambda *args, **kwargs: num_splits)
692
+ split_fn = "_decode_gfx950_num_splits" if _on_gfx950() else "_decode_num_splits"
693
+ other_split_fn = (
694
+ "_decode_num_splits" if _on_gfx950() else "_decode_gfx950_num_splits"
695
+ )
696
+ monkeypatch.setattr(mod, split_fn, lambda *args, **kwargs: num_splits)
697
+ monkeypatch.setattr(
698
+ mod, other_split_fn, lambda *args, **kwargs: pytest.fail("wrong selector")
699
+ )
700
701
actual = mod._rocm_sparse_attn_decode_ragged_triton(
702
q=q,
@@ -503,6 +726,218 @@ def test_sparse_attn_decode_split_k_kernel(
726
torch.testing.assert_close(actual, expected, atol=2e-2, rtol=2e-2)
727
728
729
+@requires_gfx950
730
+@torch.inference_mode()
731
+def test_sparse_attn_decode_gfx950_adaptive_reduce_ignores_stale_scratch() -> None:
732
+ device = torch.device("cuda")
733
+ part_m = torch.full((1, 8, 1), torch.finfo(torch.float32).min, device=device)
734
+ part_l = torch.zeros_like(part_m)
735
+ part_acc = torch.full((1, 8, 1, HEAD_DIM), float("nan"), device=device)
736
+ part_m[:, :2] = 0
737
+ part_l[:, :2] = 1
738
+ part_acc[:, 0] = 1
739
+ part_acc[:, 1] = 3
740
+
741
+ actual = _launch_sparse_decode_reduce(part_m, part_l, part_acc, True)
742
+
743
+ assert torch.isfinite(actual).all()
744
+ assert torch.equal(actual, torch.full_like(actual, 2))
745
+
746
+
747
+@requires_gfx950
748
+@pytest.mark.parametrize("extra_len", [0, 1, 31, 32, 33, 63, 64, 65])
749
+@torch.inference_mode()
750
+def test_sparse_attn_decode_gfx950_outer64_boundaries(
751
+ monkeypatch, extra_len: int
752
+) -> None:
753
+ from vllm.v1.attention.ops import rocm_aiter_mla_sparse as mod
754
+
755
+ device = torch.device("cuda")
756
+ torch.manual_seed(13)
757
+ block_size = 4
758
+ num_heads = 16
759
+ num_extra_rows = 80
760
+ q = torch.randn(2, num_heads, HEAD_DIM, dtype=torch.bfloat16, device=device)
761
+ q *= 0.125
762
+ main_cache = torch.zeros(1, block_size, 584, dtype=torch.uint8, device=device)
763
+ main_indices = torch.empty(0, dtype=torch.int32, device=device)
764
+ main_indptr = torch.zeros(3, dtype=torch.int32, device=device)
765
+ extra_cache = _pack_fp8_ds_mla_cache(
766
+ torch.randn(num_extra_rows, HEAD_DIM, dtype=torch.bfloat16, device=device)
767
+ * 0.125,
768
+ block_size,
769
+ use_fnuz=False,
770
+ )
771
+ _poison_fp8_ds_mla_cache_row(extra_cache, block_size)
772
+
773
+ raw_row = list(range(1, extra_len + 1))
774
+ if extra_len > 3:
775
+ raw_row[3] = -1
776
+ if extra_len > 40:
777
+ raw_row[40] = num_extra_rows
778
+ if extra_len > 64:
779
+ raw_row[64] = num_extra_rows + 1024
780
+ extra_indices, extra_indptr = _ragged_from_rows([raw_row, []], device)
781
+ valid_row = [slot for slot in raw_row if 0 <= slot < num_extra_rows]
782
+ attn_sink = torch.linspace(-0.1, 0.1, num_heads, dtype=torch.float32, device=device)
783
+
784
+ monkeypatch.setattr(mod, "_decode_gfx950_num_splits", lambda *args: 1)
785
+ actual = mod._rocm_sparse_attn_decode_ragged_triton(
786
+ q=q,
787
+ main_cache=main_cache,
788
+ main_indices=main_indices,
789
+ main_indptr=main_indptr,
790
+ scale=HEAD_DIM**-0.5,
791
+ attn_sink=attn_sink,
792
+ nope_head_dim=NOPE_HEAD_DIM,
793
+ rope_head_dim=ROPE_HEAD_DIM,
794
+ extra_cache=extra_cache,
795
+ extra_indices=extra_indices,
796
+ extra_indptr=extra_indptr,
797
+ )
798
+ expected = _ref_sparse_decode_ragged(
799
+ q=q,
800
+ main_cache=main_cache,
801
+ main_rows=[[], []],
802
+ scale=HEAD_DIM**-0.5,
803
+ attn_sink=attn_sink,
804
+ block_size=block_size,
805
+ extra_cache=extra_cache,
806
+ extra_rows=[valid_row, []],
807
+ )
808
+
809
+ torch.testing.assert_close(actual, expected, atol=2e-2, rtol=2e-2)
810
+ assert torch.equal(actual[1], torch.zeros_like(actual[1]))
811
+
812
+
813
+@requires_gfx950
814
+@torch.inference_mode()
815
+def test_sparse_attn_decode_gfx950_graph_replay(monkeypatch) -> None:
816
+ from vllm.v1.attention.ops import rocm_aiter_mla_sparse as mod
817
+
818
+ device = torch.device("cuda")
819
+ torch.manual_seed(17)
820
+ block_size = 64
821
+ num_queries = 16
822
+ num_heads = 16
823
+ num_splits = 8
824
+ extra_per_query = 65 * num_splits
825
+ max_extra_per_query = 8192
826
+ q = (
827
+ torch.randn(
828
+ num_queries,
829
+ num_heads,
830
+ HEAD_DIM,
831
+ dtype=torch.bfloat16,
832
+ device=device,
833
+ )
834
+ * 0.125
835
+ )
836
+ main_cache = _pack_fp8_ds_mla_cache(
837
+ torch.randn(num_queries, HEAD_DIM, dtype=torch.bfloat16, device=device) * 0.125,
838
+ block_size,
839
+ use_fnuz=False,
840
+ )
841
+ extra_cache = _pack_fp8_ds_mla_cache(
842
+ torch.randn(
843
+ num_queries * extra_per_query,
844
+ HEAD_DIM,
845
+ dtype=torch.bfloat16,
846
+ device=device,
847
+ )
848
+ * 0.125,
849
+ block_size,
850
+ use_fnuz=False,
851
+ )
852
+ main_rows = [[query_idx] for query_idx in range(num_queries)]
853
+ extra_rows = [
854
+ list(range(query_idx * extra_per_query, (query_idx + 1) * extra_per_query))
855
+ for query_idx in range(num_queries)
856
+ ]
857
+ main_indices, main_indptr = _ragged_from_rows(main_rows, device)
858
+ short_extra_rows = [row[:64] for row in extra_rows]
859
+ long_indices, long_indptr = _ragged_from_rows(extra_rows, device)
860
+ short_indices, short_indptr = _ragged_from_rows(short_extra_rows, device)
861
+ extra_indices = torch.full(
862
+ (num_queries * max_extra_per_query,),
863
+ -1,
864
+ dtype=torch.int32,
865
+ device=device,
866
+ )
867
+ extra_indices[: long_indices.numel()].copy_(long_indices)
868
+ extra_indptr = long_indptr.clone()
869
+ extra_indices_ptr = extra_indices.data_ptr()
870
+ attn_sink = torch.linspace(-0.1, 0.1, num_heads, dtype=torch.float32, device=device)
871
+ out = torch.empty_like(q)
872
+
873
+ monkeypatch.setattr(mod, "_decode_gfx950_num_splits", lambda *args: num_splits)
874
+
875
+ def run_decode() -> torch.Tensor:
876
+ return mod._rocm_sparse_attn_decode_ragged_triton(
877
+ q=q,
878
+ main_cache=main_cache,
879
+ main_indices=main_indices,
880
+ main_indptr=main_indptr,
881
+ scale=HEAD_DIM**-0.5,
882
+ attn_sink=attn_sink,
883
+ nope_head_dim=NOPE_HEAD_DIM,
884
+ rope_head_dim=ROPE_HEAD_DIM,
885
+ extra_cache=extra_cache,
886
+ extra_indices=extra_indices,
887
+ extra_indptr=extra_indptr,
888
+ out=out,
889
+ extra_cache_nan_free=True,
890
+ adaptive_splits=True,
891
+ )
892
+
893
+ run_decode()
894
+ torch.accelerator.synchronize()
895
+ graph = torch.cuda.CUDAGraph()
896
+ with torch.cuda.graph(graph):
897
+ captured_out = run_decode()
898
+ torch.accelerator.synchronize()
899
+ captured_long = out.clone()
900
+ expected_long = _ref_sparse_decode_ragged(
901
+ q=q,
902
+ main_cache=main_cache,
903
+ main_rows=main_rows,
904
+ scale=HEAD_DIM**-0.5,
905
+ attn_sink=attn_sink,
906
+ block_size=block_size,
907
+ extra_cache=extra_cache,
908
+ extra_rows=extra_rows,
909
+ )
910
+ torch.testing.assert_close(captured_long, expected_long, atol=2e-2, rtol=2e-2)
911
+
912
+ extra_indices[: short_indices.numel()].copy_(short_indices)
913
+ extra_indptr.copy_(short_indptr)
914
+ graph.replay()
915
+ torch.accelerator.synchronize()
916
+ short_out = out.clone()
917
+ expected_short = _ref_sparse_decode_ragged(
918
+ q=q,
919
+ main_cache=main_cache,
920
+ main_rows=main_rows,
921
+ scale=HEAD_DIM**-0.5,
922
+ attn_sink=attn_sink,
923
+ block_size=block_size,
924
+ extra_cache=extra_cache,
925
+ extra_rows=short_extra_rows,
926
+ )
927
+
928
+ assert captured_out.data_ptr() == out.data_ptr()
929
+ assert extra_indices.data_ptr() == extra_indices_ptr
930
+ assert extra_indices.numel() == num_queries * max_extra_per_query
931
+ assert not torch.equal(short_out, captured_long)
932
+ torch.testing.assert_close(short_out, expected_short, atol=2e-2, rtol=2e-2)
933
+
934
+ extra_indices[: long_indices.numel()].copy_(long_indices)
935
+ extra_indptr.copy_(long_indptr)
936
+ graph.replay()
937
+ torch.accelerator.synchronize()
938
+ torch.testing.assert_close(out, expected_long, atol=2e-2, rtol=2e-2)
939
+
940
+
941
# ---------------------------------------------------------------------------
942
# o-projection: fused inverse-RoPE + cached bf16 wo_a (rocm_inv_rope_einsum)
943
# ---------------------------------------------------------------------------
tests/kernels/test_compressor_kv_cache.py
+239
-4
@@ -28,16 +28,81 @@ from vllm.models.deepseek_v4.common.ops.fused_compress_quant_cache import (
28
_fused_kv_compress_norm_rope_insert_indexer_attn,
29
_fused_kv_compress_norm_rope_insert_indexer_mxfp4_attn,
30
_launch_two_stage_sparse_attn_compressor,
31
+ compress_norm_rope_store_triton,
32
)
33
from vllm.models.deepseek_v4.compressor import _get_c128_boundary
34
from vllm.platforms import current_platform
35
from vllm.v1.attention.backends.mla.compressor_utils import (
36
get_dspark_swa_index_width,
37
)
38
+from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
39
+ cp_gather_indexer_k_quant_cache_triton,
40
+ indexer_k_quant_and_cache_triton,
41
+)
42
43
from .test_fused_indexer_q_rope_quant import quantize_to_mxfp4
44
45
46
+def _on_gfx950() -> bool:
47
+ if not current_platform.is_rocm():
48
+ return False
49
+ try:
50
+ from vllm.platforms.rocm import _ON_GFX950
51
+
52
+ return _ON_GFX950
53
+ except Exception:
54
+ return False
55
+
56
+
57
+@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm-only dispatch")
58
+def test_cp_gather_despecialized_kernel_is_gfx950_only(monkeypatch):
59
+ from vllm.v1.attention.ops import rocm_aiter_mla_sparse as mod
60
+
61
+ class FakeKernel:
62
+ def __init__(self):
63
+ self.calls = []
64
+
65
+ def __getitem__(self, grid):
66
+ def launch(*args):
67
+ self.calls.append((grid, args))
68
+
69
+ return launch
70
+
71
+ legacy_kernel = FakeKernel()
72
+ gfx950_kernel = FakeKernel()
73
+ monkeypatch.setattr(mod, "_cp_gather_indexer_quant_cache_kernel", legacy_kernel)
74
+ monkeypatch.setattr(
75
+ mod,
76
+ "_cp_gather_indexer_quant_cache_gfx950_kernel",
77
+ gfx950_kernel,
78
+ )
79
+
80
+ k_cache = torch.zeros((4, 1, 132), dtype=torch.uint8)
81
+ k_fp8 = torch.empty((5, 128), dtype=current_platform.fp8_dtype())
82
+ k_scale = torch.empty((5, 4), dtype=torch.uint8)
83
+ block_table = torch.zeros((2, 7), dtype=torch.int32)
84
+ cu_seqlen = torch.tensor([0, 2, 5], dtype=torch.int32)
85
+ token_to_seq = torch.tensor([0, 0, 1, 1, 1], dtype=torch.int32)
86
+ args = (k_cache, k_fp8, k_scale, block_table, cu_seqlen, token_to_seq)
87
+
88
+ monkeypatch.setattr(mod, "_ON_GFX950", True)
89
+ mod.cp_gather_indexer_k_quant_cache_triton(*args)
90
+ assert len(gfx950_kernel.calls) == 1
91
+ assert not legacy_kernel.calls
92
+ gfx950_grid, gfx950_args = gfx950_kernel.calls[0]
93
+ assert gfx950_grid == (5,)
94
+ assert len(gfx950_args) == 18
95
+ assert gfx950_args[-3:] == (2, 7, 4)
96
+
97
+ monkeypatch.setattr(mod, "_ON_GFX950", False)
98
+ mod.cp_gather_indexer_k_quant_cache_triton(*args)
99
+ assert len(legacy_kernel.calls) == 1
100
+ legacy_grid, legacy_args = legacy_kernel.calls[0]
101
+ assert legacy_grid == (5,)
102
+ assert len(legacy_args) == 19
103
+ assert legacy_args[-4:] == (5, 2, 7, 4)
104
+
105
+
106
@pytest.mark.parametrize(
107
("window_size", "num_speculative_tokens", "expected"),
108
[(128, 5, 192), (512, 5, 576), (1024, 0, 1024)],
@@ -91,6 +156,129 @@ def _ue8m0_reference(x: torch.Tensor, block_size: int, fp8_max: float):
156
return x_fp8, scales
157
158
159
+def _decode_dsv4_cache_row(
160
+ cache: torch.Tensor, block_size: int, scrub_nan: bool
161
+) -> torch.Tensor:
162
+ flat = cache.flatten()
163
+ nope = flat[:448].view(torch.float8_e4m3fn).to(torch.bfloat16)
164
+ encoded = flat[block_size * 576 : block_size * 576 + 7]
165
+ scales = torch.exp2(encoded.to(torch.float32) - 127.0).to(torch.bfloat16)
166
+ nope = nope * scales.repeat_interleave(64)
167
+ rope = flat[448:576].view(torch.bfloat16)
168
+ decoded = torch.cat((nope, rope))
169
+ if scrub_nan:
170
+ decoded = torch.where(decoded == decoded, decoded, 0.0)
171
+ return decoded
172
+
173
+
174
+def _assert_nan_free_cache_matches_legacy_scrub(
175
+ cache: torch.Tensor, block_size: int
176
+) -> None:
177
+ flat = cache.flatten()
178
+ scale_base = block_size * 576
179
+ scale_codes = flat[scale_base : scale_base + 8]
180
+ assert scale_codes[0].item() == 254
181
+ assert scale_codes[1].item() == 247
182
+ assert scale_codes[:7].max().item() <= 254
183
+ nope_bytes = flat[:448]
184
+ assert not ((nope_bytes == 0x7F) | (nope_bytes == 0xFF)).any()
185
+
186
+ rope = flat[448:576].view(torch.bfloat16)
187
+ assert not torch.isnan(rope).any()
188
+ assert torch.isposinf(rope[0])
189
+ assert torch.equal(rope[1:4], torch.zeros_like(rope[1:4]))
190
+
191
+ legacy_cache = cache.clone()
192
+ legacy_flat = legacy_cache.flatten()
193
+ legacy_flat[scale_base] = 255
194
+ legacy_rope = legacy_flat[448:576].view(torch.bfloat16)
195
+ legacy_rope[1:4] = float("nan")
196
+ canonical = _decode_dsv4_cache_row(cache, block_size, scrub_nan=False)
197
+ legacy = _decode_dsv4_cache_row(legacy_cache, block_size, scrub_nan=True)
198
+ torch.testing.assert_close(canonical, legacy, rtol=0, atol=0)
199
+ assert torch.isinf(canonical[0])
200
+ assert torch.isposinf(canonical[64])
201
+
202
+
203
+@pytest.mark.skipif(
204
+ not _on_gfx950(),
205
+ reason="NaN-free fp8_ds_mla compressed-cache contract is gfx950-only",
206
+)
207
+@pytest.mark.parametrize("writer", ["single_pass", "two_stage_finalizer"])
208
+def test_gfx950_compressed_cache_canonicalizes_nonfinite(writer: str) -> None:
209
+ head_dim = 512
210
+ rope_dim = 64
211
+ block_size = 4
212
+ device = "cuda"
213
+
214
+ positions = torch.zeros(1, dtype=torch.int64, device=device)
215
+ slot_mapping = torch.zeros(1, dtype=torch.int64, device=device)
216
+ rms_weight = torch.ones(head_dim, dtype=torch.bfloat16, device=device)
217
+ rms_weight[0] = float("inf")
218
+ rms_weight[64] = torch.finfo(torch.bfloat16).max
219
+ rms_weight[448] = float("inf")
220
+ rms_weight[450] = float("nan")
221
+ cos_sin_cache = torch.zeros(1, rope_dim, dtype=torch.float32, device=device)
222
+ cos_sin_cache[:, : rope_dim // 2] = 1.0
223
+ cache = torch.zeros(1, block_size, 584, dtype=torch.uint8, device=device)
224
+
225
+ state_cache = torch.zeros(1, 1, 2 * head_dim, dtype=torch.float32, device=device)
226
+ state_cache[..., :head_dim] = 1.0
227
+ token_to_req = torch.zeros(1, dtype=torch.int32, device=device)
228
+ block_table = torch.zeros(1, 1, dtype=torch.int32, device=device)
229
+
230
+ if writer == "single_pass":
231
+ compress_norm_rope_store_triton(
232
+ state_cache=state_cache,
233
+ num_actual=1,
234
+ token_to_req_indices=token_to_req,
235
+ positions=positions,
236
+ slot_mapping=slot_mapping,
237
+ block_table=block_table,
238
+ block_size=1,
239
+ state_width=head_dim,
240
+ cos_sin_cache=cos_sin_cache,
241
+ kv_cache=cache,
242
+ k_cache_metadata=SimpleNamespace(slot_mapping=slot_mapping),
243
+ pdl_kwargs={},
244
+ head_dim=head_dim,
245
+ rope_head_dim=rope_dim,
246
+ compress_ratio=1,
247
+ overlap=False,
248
+ use_fp4_cache=False,
249
+ rms_norm_weight=rms_weight,
250
+ rms_norm_eps=1e-6,
251
+ quant_block=64,
252
+ token_stride=576,
253
+ scale_dim=8,
254
+ )
255
+ else:
256
+ _launch_two_stage_sparse_attn_compressor(
257
+ state_cache,
258
+ token_to_req,
259
+ positions,
260
+ slot_mapping,
261
+ block_table,
262
+ 1,
263
+ head_dim,
264
+ 1,
265
+ cos_sin_cache,
266
+ cache,
267
+ slot_mapping,
268
+ rms_weight,
269
+ 1e-6,
270
+ 64,
271
+ 576,
272
+ 8,
273
+ head_dim,
274
+ rope_dim,
275
+ 1,
276
+ torch.empty(1, head_dim, dtype=torch.float32, device=device),
277
+ )
278
+
279
+ _assert_nan_free_cache_matches_legacy_scrub(cache, block_size)
280
+
281
+
282
@pytest.mark.parametrize(
283
("starts", "query_start_loc", "expected"),
284
[
@@ -402,7 +590,8 @@ def test_indexer_gather_accepts_upper_bound_output():
590
valid_tokens = 9
591
upper_bound_tokens = 13
592
block_size = 16
405
- num_blocks = 2
593
+ num_seqs = 3
594
+ num_blocks = num_seqs
595
sentinel = 123
596
device = "cuda"
597
@@ -410,13 +599,15 @@ def test_indexer_gather_accepts_upper_bound_output():
599
kv_cache = torch.zeros(
600
num_blocks, block_size, cache_stride, dtype=torch.uint8, device=device
601
)
413
- slot_mapping = torch.arange(valid_tokens, dtype=torch.int64, device=device)
602
+ slot_mapping = torch.tensor(
603
+ [0, 1, 2, 16, 17, 18, 32, 33, 34], dtype=torch.int64, device=device
604
+ )
605
ops.indexer_k_quant_and_cache(k, kv_cache, slot_mapping, quant_block_size, "ue8m0")
606
607
block_table = torch.arange(num_blocks, dtype=torch.int32, device=device).unsqueeze(
417
- 0
608
+ 1
609
)
419
- cu_seq_lens = torch.tensor([0, valid_tokens], dtype=torch.int32, device=device)
610
+ cu_seq_lens = torch.tensor([0, 3, 6, 9], dtype=torch.int32, device=device)
611
dst_k = torch.full(
612
(upper_bound_tokens, head_dim), sentinel, dtype=torch.uint8, device=device
613
)
@@ -431,8 +622,52 @@ def test_indexer_gather_accepts_upper_bound_output():
622
ops.cp_gather_indexer_k_quant_cache(
623
kv_cache, dst_k, dst_scale, block_table, cu_seq_lens
624
)
625
+
626
+ if current_platform.is_rocm():
627
+ triton_kv_cache = torch.zeros_like(kv_cache)
628
+ indexer_k_quant_and_cache_triton(
629
+ k,
630
+ triton_kv_cache,
631
+ slot_mapping,
632
+ quant_block_size,
633
+ "ue8m0",
634
+ )
635
+ triton_dst_k = torch.full_like(dst_k, sentinel)
636
+ triton_dst_scale = torch.full_like(dst_scale, sentinel)
637
+ token_to_seq = torch.cat(
638
+ (
639
+ torch.repeat_interleave(
640
+ torch.arange(num_seqs, dtype=torch.int32, device=device), 3
641
+ ),
642
+ torch.full(
643
+ (upper_bound_tokens - valid_tokens,),
644
+ -1,
645
+ dtype=torch.int32,
646
+ device=device,
647
+ ),
648
+ )
649
+ )
650
+ cp_gather_indexer_k_quant_cache_triton(
651
+ triton_kv_cache,
652
+ triton_dst_k.view(current_platform.fp8_dtype()),
653
+ triton_dst_scale,
654
+ block_table,
655
+ cu_seq_lens,
656
+ token_to_seq,
657
+ )
658
torch.accelerator.synchronize()
659
660
+ if current_platform.is_rocm():
661
+ triton_recovered = triton_dst_k[:valid_tokens].view(
662
+ current_platform.fp8_dtype()
663
+ ).float() * triton_dst_scale[:valid_tokens].view(torch.float32)
664
+ triton_error = (triton_recovered - k.float()).abs().amax(dim=1)
665
+ max_triton_error = (
666
+ 16.0 * triton_dst_scale[:valid_tokens].view(torch.float32).flatten()
667
+ )
668
+ assert torch.all(triton_error <= max_triton_error)
669
+ assert torch.all(triton_dst_k[valid_tokens:] == sentinel)
670
+ assert torch.all(triton_dst_scale[valid_tokens:] == sentinel)
671
k_recovered = dst_k[:valid_tokens].view(torch.float8_e4m3fn).float() * dst_scale[
672
:valid_tokens
673
].view(torch.float32)
vllm/models/deepseek_v4/amd/rocm.py
+48
-3
@@ -19,6 +19,7 @@ from vllm.models.deepseek_v4.sparse_mla import (
19
DeepseekV4SparseMLAMetadataBuilder,
20
)
21
from vllm.platforms import current_platform
22
+from vllm.platforms.rocm import _ON_GFX950
23
from vllm.triton_utils import tl, triton
24
from vllm.v1.attention.backend import (
25
CommonAttentionMetadata,
@@ -36,6 +37,19 @@ from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
37
from vllm.v1.worker.workspace import current_workspace_manager
38
39
40
+def _trust_dsv4_extra_cache_nan_free(
41
+ kv_cache_dtype: str,
42
+ has_kv_transfer: bool,
43
+ has_extra_cache: bool,
44
+) -> bool:
45
+ return (
46
+ _ON_GFX950
47
+ and kv_cache_dtype == "fp8_ds_mla"
48
+ and not has_kv_transfer
49
+ and has_extra_cache
50
+ )
51
+
52
+
53
def _build_indptr_from_lengths(lengths: torch.Tensor) -> torch.Tensor:
54
lengths = lengths.to(dtype=torch.int32).contiguous()
55
indptr = torch.zeros(lengths.shape[0] + 1, dtype=torch.int32, device=lengths.device)
@@ -294,9 +308,13 @@ def _copy_ragged_to_graph_buffers(
308
309
max_entries = max(num_rows * max_entries_per_row, 1)
310
ragged_out = ragged_indices_buffer[:max_entries]
297
- nnz = ragged_indices.numel()
298
- if nnz > 0:
299
- ragged_out[:nnz].copy_(ragged_indices, non_blocking=True)
311
+ source_entries = ragged_indices.numel()
312
+ if source_entries > 0:
313
+ ragged_out[:source_entries].copy_(ragged_indices, non_blocking=True)
314
+ if _ON_GFX950:
315
+ # Preserve the graph-stable base pointer while exposing source capacity
316
+ # to the sync-free split selector; indptr still carries the true NNZ.
317
+ ragged_out = ragged_out[: max(source_entries, 1)]
318
return ragged_out, indptr_out
319
320
@@ -306,6 +324,7 @@ class DeepseekV4ROCMAiterMLASparseMetadata(DeepseekV4FlashMLAMetadata):
324
325
c128a_decode_topk_ragged_indices: torch.Tensor | None = None
326
c128a_decode_topk_ragged_indptr: torch.Tensor | None = None
327
+ for_cudagraph_capture: bool = False
328
329
330
@dataclass
@@ -370,6 +389,16 @@ class DeepseekV4ROCMAiterMLASparseMetadataBuilder(DeepseekV4SparseMLAMetadataBui
389
c128a_decode_topk_ragged_indptr=ragged_indptr,
390
)
391
392
+ def build_for_cudagraph_capture(
393
+ self, common_attn_metadata: CommonAttentionMetadata
394
+ ) -> DeepseekV4ROCMAiterMLASparseMetadata:
395
+ metadata = cast(
396
+ DeepseekV4ROCMAiterMLASparseMetadata,
397
+ super().build_for_cudagraph_capture(common_attn_metadata),
398
+ )
399
+ metadata.for_cudagraph_capture = _ON_GFX950
400
+ return metadata
401
+
402
403
class DeepseekV4ROCMAiterSparseSWAMetadataBuilder(DeepseekSparseSWAMetadataBuilder):
404
# Keep fused multi-step decode disabled until update_draft_decode_metadata()
@@ -451,7 +480,9 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
480
backend_cls = DeepseekV4ROCMAiterMLASparseBackend
481
482
def __init__(self, *args, **kwargs):
483
+ vllm_config = args[0] if args else kwargs["vllm_config"]
484
super().__init__(*args, **kwargs)
485
+ self._has_kv_transfer = vllm_config.kv_transfer_config is not None
486
# Block scale for the preshuffled weight; None = not preshuffled.
487
self._wqa_wkv_scale: torch.Tensor | None = None
488
self._wo_b_scale: torch.Tensor | None = None
@@ -605,6 +636,13 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
636
attn_metadata=rocm_metadata,
637
swa_only=swa_only,
638
output=output[:num_decode_tokens],
639
+ adaptive_splits=(
640
+ _ON_GFX950
641
+ and not swa_only
642
+ and self.compress_ratio == 128
643
+ and rocm_metadata is not None
644
+ and rocm_metadata.for_cudagraph_capture
645
+ ),
646
)
647
648
def _forward_decode(
@@ -615,6 +653,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
653
attn_metadata: DeepseekV4ROCMAiterMLASparseMetadata | None,
654
swa_only: bool,
655
output: torch.Tensor,
656
+ adaptive_splits: bool,
657
) -> None:
658
num_decodes = swa_metadata.num_decodes
659
num_decode_tokens = swa_metadata.num_decode_tokens
@@ -666,6 +705,12 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
705
nope_head_dim=self.nope_head_dim,
706
rope_head_dim=self.rope_head_dim,
707
output=output,
708
+ adaptive_splits=adaptive_splits,
709
+ extra_cache_nan_free=_trust_dsv4_extra_cache_nan_free(
710
+ self.kv_cache_dtype,
711
+ self._has_kv_transfer,
712
+ not swa_only and kv_cache is not None,
713
+ ),
714
)
715
716
def _forward_prefill(
vllm/models/deepseek_v4/common/ops/fused_compress_quant_cache.py
+21
-2
@@ -24,8 +24,14 @@ from typing import Any
24
25
import torch
26
27
+from vllm.platforms import current_platform
28
from vllm.triton_utils import tl, triton
29
30
+if current_platform.is_rocm():
31
+ from vllm.platforms.rocm import _ON_GFX950
32
+else:
33
+ _ON_GFX950 = False
34
+
35
from .fused_indexer_q import _fp32x2_to_fp4x2
36
37
@@ -61,12 +67,15 @@ def compress_norm_rope_store_triton(
67
if head_dim == 512:
68
kernel = _fused_kv_compress_norm_rope_insert_sparse_attn
69
num_warps = 4
70
+ kernel_kwargs = {"SANITIZE_CACHE_NANS": _ON_GFX950}
71
elif use_fp4_cache:
72
kernel = _fused_kv_compress_norm_rope_insert_indexer_mxfp4_attn
73
num_warps = 1
74
+ kernel_kwargs = {}
75
else:
76
kernel = _fused_kv_compress_norm_rope_insert_indexer_attn
77
num_warps = 1
78
+ kernel_kwargs = {}
79
80
kernel[(num_actual,)](
81
# state cache
@@ -103,6 +112,7 @@ def compress_norm_rope_store_triton(
112
SCALE_DIM=scale_dim,
113
KV_BLOCK_STRIDE=kv_cache.stride(0),
114
num_warps=num_warps,
115
+ **kernel_kwargs,
116
**pdl_kwargs,
117
)
118
@@ -145,6 +155,7 @@ def _fused_kv_compress_norm_rope_insert_sparse_attn(
155
TOKEN_STRIDE: tl.constexpr, # 576 for DeepseekV4
156
SCALE_DIM: tl.constexpr, # 8 for DeepseekV4 (7 real + 1 pad)
157
KV_BLOCK_STRIDE: tl.constexpr,
158
+ SANITIZE_CACHE_NANS: tl.constexpr,
159
):
160
"""Fused compress → RMSNorm → FP8 quant (nope) → RoPE → bf16 store (rope).
161
@@ -261,7 +272,8 @@ def _fused_kv_compress_norm_rope_insert_sparse_attn(
272
273
scale_idx = tl.arange(0, N_QUANT_BLOCKS)
274
encoded = exponents + 127.0
264
- encoded = tl.maximum(tl.minimum(encoded, 255.0), 0.0)
275
+ max_encoded: tl.constexpr = 254.0 if SANITIZE_CACHE_NANS else 255.0
276
+ encoded = tl.maximum(tl.minimum(encoded, max_encoded), 0.0)
277
tl.store(
278
scale_ptr + scale_idx,
279
encoded.to(tl.uint8),
@@ -289,6 +301,8 @@ def _fused_kv_compress_norm_rope_insert_sparse_attn(
301
new_even = even * cos_v - odd * sin_v
302
new_odd = odd * cos_v + even * sin_v
303
result = tl.interleave(new_even, new_odd) # [TRITON_BLOCK_SIZE] fp32
304
+ if SANITIZE_CACHE_NANS:
305
+ result = tl.where(result == result, result, 0.0)
306
307
# Store rotated rope portion as bf16 into the cache's bf16 area.
308
bf16_ptr = (fp8_ptr + NOPE_HEAD_DIM).to(tl.pointer_type(tl.bfloat16))
@@ -417,6 +431,7 @@ def _finalize_norm_rope_quant_store_sparse_attn(
431
TOKEN_STRIDE: tl.constexpr,
432
SCALE_DIM: tl.constexpr,
433
KV_BLOCK_STRIDE: tl.constexpr,
434
+ SANITIZE_CACHE_NANS: tl.constexpr,
435
):
436
"""Stage 2: read compressed_kv[512] from scratch buffer, then
437
RMSNorm + FP8 quant (nope) + RoPE + bf16 store
@@ -474,7 +489,8 @@ def _finalize_norm_rope_quant_store_sparse_attn(
489
tl.store(fp8_ptr + block, x_uint8, mask=block < NOPE_HEAD_DIM)
490
491
scale_idx = tl.arange(0, N_QUANT_BLOCKS)
477
- encoded = tl.maximum(tl.minimum(exponents + 127.0, 255.0), 0.0)
492
+ max_encoded: tl.constexpr = 254.0 if SANITIZE_CACHE_NANS else 255.0
493
+ encoded = tl.maximum(tl.minimum(exponents + 127.0, max_encoded), 0.0)
494
tl.store(
495
scale_ptr + scale_idx, encoded.to(tl.uint8), mask=scale_idx < N_NOPE_BLOCKS
496
)
@@ -494,6 +510,8 @@ def _finalize_norm_rope_quant_store_sparse_attn(
510
new_even = even * cos_v - odd * sin_v
511
new_odd = odd * cos_v + even * sin_v
512
result = tl.interleave(new_even, new_odd)
513
+ if SANITIZE_CACHE_NANS:
514
+ result = tl.where(result == result, result, 0.0)
515
bf16_ptr = (fp8_ptr + NOPE_HEAD_DIM).to(tl.pointer_type(tl.bfloat16))
516
rope_local = block - NOPE_HEAD_DIM
517
is_rope = (block >= NOPE_HEAD_DIM) & mask
@@ -564,6 +582,7 @@ def _launch_two_stage_sparse_attn_compressor(
582
TOKEN_STRIDE=token_stride,
583
SCALE_DIM=scale_dim,
584
KV_BLOCK_STRIDE=kv_cache.stride(0),
585
+ SANITIZE_CACHE_NANS=_ON_GFX950,
586
)
587
588
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+784
-62
@@ -214,6 +214,87 @@ def _cp_gather_indexer_quant_cache_kernel(
214
tl.store(dst_k_ptr + offset, val, mask=valid_block)
215
216
217
+@triton.jit(do_not_specialize=["num_batches"])
218
+def _cp_gather_indexer_quant_cache_gfx950_kernel(
219
+ kv_cache_ptr, # [n_blks,blk_size//tile_blk,head_dim//16B,tile_blk,16B]
220
+ # [n_blks, blk_size, head_dim]
221
+ kv_cache_scale_ptr, # [n_blks, blk_size]
222
+ k_fp8_ptr, # [num_tokens, head_dim]
223
+ k_scale_ptr, # [num_tokens]
224
+ block_table_ptr, # [batch_size, block_table_stride]
225
+ cu_seqlen_ptr, # [batch_size + 1]
226
+ token_to_seq_ptr, # [num_tokens]
227
+ block_size,
228
+ block_table_stride,
229
+ kv_cache_stride,
230
+ kv_cache_scale_stride,
231
+ LAYOUT: tl.constexpr,
232
+ HEAD_DIM: tl.constexpr,
233
+ BLOCK_TILE_SIZE: tl.constexpr,
234
+ HEAD_TILE_SIZE: tl.constexpr,
235
+ num_batches,
236
+ BLOCK_TABLE_WIDTH: tl.constexpr,
237
+ NUM_BLOCKS: tl.constexpr,
238
+):
239
+ tid = tl.program_id(0)
240
+ offset = tl.arange(0, HEAD_DIM)
241
+ batch_id = tl.load(token_to_seq_ptr + tid)
242
+ valid_batch = (batch_id >= 0) & (batch_id < num_batches)
243
+ safe_batch_id = tl.where(valid_batch, batch_id, 0)
244
+ batch_start = tl.load(cu_seqlen_ptr + safe_batch_id, mask=valid_batch, other=0)
245
+ batch_end = tl.load(cu_seqlen_ptr + safe_batch_id + 1, mask=valid_batch, other=0)
246
+ batch_offset = tid - batch_start
247
+ valid_token = valid_batch & (tid >= batch_start) & (tid < batch_end)
248
+ if not valid_token:
249
+ return
250
+ block_table_id = batch_offset // block_size
251
+ block_offset = batch_offset % block_size
252
+ valid_block_table = (
253
+ valid_token
254
+ & (block_table_id >= 0)
255
+ & (block_table_id < BLOCK_TABLE_WIDTH)
256
+ & (block_offset >= 0)
257
+ & (block_offset < block_size)
258
+ )
259
+ safe_block_table_id = tl.where(valid_block_table, block_table_id, 0)
260
+ block_table_offset = safe_batch_id * block_table_stride + safe_block_table_id
261
+ block_id = tl.load(
262
+ block_table_ptr + block_table_offset, mask=valid_block_table, other=-1
263
+ )
264
+ valid_block = valid_block_table & (block_id >= 0) & (block_id < NUM_BLOCKS)
265
+ # The packed KV layout makes per-block strides large
266
+ # enough that block_id * stride can exceed 32-bit range.
267
+ safe_block_id = tl.where(valid_block, block_id, 0).to(tl.int64)
268
+ safe_block_offset = tl.where(valid_block, block_offset, 0)
269
+ tiled_block_offset = safe_block_offset % BLOCK_TILE_SIZE
270
+ if LAYOUT == "SHUFFLE":
271
+ src_cache_offset = (
272
+ safe_block_id * kv_cache_stride
273
+ + (safe_block_offset // BLOCK_TILE_SIZE) * HEAD_DIM * BLOCK_TILE_SIZE
274
+ + tiled_block_offset * HEAD_TILE_SIZE
275
+ )
276
+ else:
277
+ src_cache_offset = (
278
+ safe_block_id * kv_cache_stride + safe_block_offset * HEAD_DIM
279
+ )
280
+ src_scale_offset = safe_block_id * kv_cache_scale_stride + safe_block_offset
281
+ dst_offset = tid * HEAD_DIM
282
+ src_scale_ptr = kv_cache_scale_ptr + src_scale_offset
283
+ src_cache_ptr = kv_cache_ptr + src_cache_offset
284
+ dst_k_ptr = k_fp8_ptr + dst_offset
285
+ scale_val = tl.load(src_scale_ptr, mask=valid_block, other=0.0)
286
+ tl.store(k_scale_ptr + tid, scale_val)
287
+ if LAYOUT == "SHUFFLE":
288
+ tiled_src_offset = (
289
+ offset // HEAD_TILE_SIZE * HEAD_TILE_SIZE * BLOCK_TILE_SIZE
290
+ + offset % HEAD_TILE_SIZE
291
+ )
292
+ else:
293
+ tiled_src_offset = offset
294
+ val = tl.load(src_cache_ptr + tiled_src_offset)
295
+ tl.store(dst_k_ptr + offset, val, mask=valid_block)
296
+
297
+
298
def cp_gather_indexer_k_quant_cache_triton(
299
k_cache: torch.Tensor, # [num_blocks, block_size, head_dim + 4]
300
k_fp8: torch.Tensor,
@@ -237,7 +318,7 @@ def cp_gather_indexer_k_quant_cache_triton(
318
grid = (num_tokens,)
319
k_fp8_scale = k_fp8_scale.view(torch.float32)
320
layout = "NORMAL" if block_size == 1 else "SHUFFLE"
240
- _cp_gather_indexer_quant_cache_kernel[grid](
321
+ kernel_args = (
322
k_cache_value,
323
k_cache_scale,
324
k_fp8,
@@ -253,11 +334,22 @@ def cp_gather_indexer_k_quant_cache_triton(
334
head_dim,
335
block_tile_size,
336
head_tile_size,
256
- num_tokens,
257
- cu_seqlen.shape[0] - 1,
258
- block_table.shape[1],
259
- num_blocks,
337
)
338
+ if _ON_GFX950:
339
+ _cp_gather_indexer_quant_cache_gfx950_kernel[grid](
340
+ *kernel_args,
341
+ cu_seqlen.shape[0] - 1,
342
+ block_table.shape[1],
343
+ num_blocks,
344
+ )
345
+ else:
346
+ _cp_gather_indexer_quant_cache_kernel[grid](
347
+ *kernel_args,
348
+ num_tokens,
349
+ cu_seqlen.shape[0] - 1,
350
+ block_table.shape[1],
351
+ num_blocks,
352
+ )
353
354
355
# Taken from https://github.com/deepseek-ai/DeepGEMM/blob/main/tests/test_attention.py#L156
@@ -1211,6 +1303,90 @@ def _sparse_attn_prefill_ragged_kernel(
1303
)
1304
1305
1306
+@triton.jit
1307
+def _decode_e8m0_scales_triton(encoded_scales):
1308
+ scale_bits = encoded_scales.to(tl.int32) << 23
1309
+ scale_bits = tl.where(encoded_scales == 0, 1 << 22, scale_bits)
1310
+ return scale_bits.to(tl.float32, bitcast=True)
1311
+
1312
+
1313
+@triton.jit
1314
+def _load_fp8_ds_mla_gfx950_nope_exact_chunk(
1315
+ token_data_ptr,
1316
+ token_scale_ptr,
1317
+ valid,
1318
+ CHUNK_START: tl.constexpr,
1319
+ CHUNK_SIZE: tl.constexpr,
1320
+ BLOCK_K: tl.constexpr,
1321
+ IS_FNUZ: tl.constexpr,
1322
+):
1323
+ offsets = CHUNK_START + tl.arange(0, CHUNK_SIZE)
1324
+ x_uint8 = tl.load(
1325
+ token_data_ptr[:, None] + offsets[None, :],
1326
+ mask=valid[:, None],
1327
+ other=0,
1328
+ )
1329
+ scale_offsets = CHUNK_START // 64 + tl.arange(0, CHUNK_SIZE // 64)
1330
+ encoded_scales = tl.load(
1331
+ token_scale_ptr[:, None] + scale_offsets[None, :],
1332
+ mask=valid[:, None],
1333
+ other=127,
1334
+ )
1335
+ scales = _decode_e8m0_scales_triton(encoded_scales)
1336
+ scales = tl.broadcast_to(scales[:, :, None], (BLOCK_K, CHUNK_SIZE // 64, 64))
1337
+ scales = tl.reshape(scales, (BLOCK_K, CHUNK_SIZE))
1338
+ if IS_FNUZ:
1339
+ x_f32 = x_uint8.to(tl.float8e4b8, bitcast=True).to(tl.bfloat16).to(tl.float32)
1340
+ else:
1341
+ x_f32 = x_uint8.to(tl.float8e4nv, bitcast=True).to(tl.float32)
1342
+ value = (x_f32 * scales).to(tl.bfloat16)
1343
+ zero = tl.zeros((BLOCK_K, CHUNK_SIZE), dtype=tl.bfloat16)
1344
+ return tl.where(valid[:, None], value, zero)
1345
+
1346
+
1347
+@triton.jit
1348
+def _load_fp8_ds_mla_gfx950_tail128(
1349
+ token_data_ptr,
1350
+ token_scale_ptr,
1351
+ valid,
1352
+ NOPE_DIM: tl.constexpr,
1353
+ BLOCK_K: tl.constexpr,
1354
+ IS_FNUZ: tl.constexpr,
1355
+):
1356
+ tail_offsets = 384 + tl.arange(0, 128)
1357
+ nope_mask = tail_offsets < NOPE_DIM
1358
+ x_uint8 = tl.load(
1359
+ token_data_ptr[:, None] + tail_offsets[None, :],
1360
+ mask=valid[:, None] & nope_mask[None, :],
1361
+ other=0,
1362
+ )
1363
+ scale_offsets = 6 + tl.arange(0, 2)
1364
+ scale_mask = scale_offsets < NOPE_DIM // 64
1365
+ encoded_scales = tl.load(
1366
+ token_scale_ptr[:, None] + scale_offsets[None, :],
1367
+ mask=valid[:, None] & scale_mask[None, :],
1368
+ other=127,
1369
+ )
1370
+ scales = _decode_e8m0_scales_triton(encoded_scales)
1371
+ scales = tl.broadcast_to(scales[:, :, None], (BLOCK_K, 2, 64))
1372
+ scales = tl.reshape(scales, (BLOCK_K, 128))
1373
+ if IS_FNUZ:
1374
+ x_f32 = x_uint8.to(tl.float8e4b8, bitcast=True).to(tl.bfloat16).to(tl.float32)
1375
+ else:
1376
+ x_f32 = x_uint8.to(tl.float8e4nv, bitcast=True).to(tl.float32)
1377
+ nope = (x_f32 * scales).to(tl.bfloat16)
1378
+
1379
+ rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16))
1380
+ rope = tl.load(
1381
+ rope_ptr[:, None] + (tail_offsets[None, :] - NOPE_DIM),
1382
+ mask=valid[:, None] & ~nope_mask[None, :],
1383
+ other=0.0,
1384
+ )
1385
+ value = tl.where(nope_mask[None, :], nope, rope)
1386
+ zero = tl.zeros((BLOCK_K, 128), dtype=tl.bfloat16)
1387
+ return tl.where(valid[:, None], value, zero)
1388
+
1389
+
1390
@triton.jit
1391
def _sparse_attn_decode_ragged_kernel(
1392
q_ptr,
@@ -1685,6 +1861,407 @@ def _sparse_attn_decode_partial_kernel(
1861
)
1862
1863
1864
+@triton.jit
1865
+def _sparse_attn_decode_gfx950_partial_loaded_tile(
1866
+ q_combined,
1867
+ cache_ptr,
1868
+ slot,
1869
+ valid,
1870
+ cache_stride0,
1871
+ scale: tl.constexpr,
1872
+ head_mask,
1873
+ m_i,
1874
+ l_i,
1875
+ acc_nope_0a,
1876
+ acc_nope_0b,
1877
+ acc_nope_1,
1878
+ acc_tail,
1879
+ BLOCK_SIZE: tl.constexpr,
1880
+ NOPE_DIM: tl.constexpr,
1881
+ BLOCK_K: tl.constexpr,
1882
+ IS_FNUZ: tl.constexpr,
1883
+ TRUST_EXTRA_CACHE_NAN_FREE: tl.constexpr,
1884
+):
1885
+ safe_slot = tl.where(valid, slot, 0)
1886
+ block_idx = safe_slot // BLOCK_SIZE
1887
+ pos_in_block = safe_slot % BLOCK_SIZE
1888
+ cache_block_ptr = cache_ptr + block_idx.to(tl.int64) * cache_stride0
1889
+ token_data_ptr = cache_block_ptr + pos_in_block * 576
1890
+ token_scale_ptr = cache_block_ptr + BLOCK_SIZE * 576 + pos_in_block * 8
1891
+ k_nope_0a = _load_fp8_ds_mla_gfx950_nope_exact_chunk(
1892
+ token_data_ptr,
1893
+ token_scale_ptr,
1894
+ valid,
1895
+ 0,
1896
+ 128,
1897
+ BLOCK_K,
1898
+ IS_FNUZ,
1899
+ )
1900
+ k_nope_0b = _load_fp8_ds_mla_gfx950_nope_exact_chunk(
1901
+ token_data_ptr,
1902
+ token_scale_ptr,
1903
+ valid,
1904
+ 128,
1905
+ 128,
1906
+ BLOCK_K,
1907
+ IS_FNUZ,
1908
+ )
1909
+ k_nope_1 = _load_fp8_ds_mla_gfx950_nope_exact_chunk(
1910
+ token_data_ptr,
1911
+ token_scale_ptr,
1912
+ valid,
1913
+ 256,
1914
+ 128,
1915
+ BLOCK_K,
1916
+ IS_FNUZ,
1917
+ )
1918
+ k_tail = _load_fp8_ds_mla_gfx950_tail128(
1919
+ token_data_ptr,
1920
+ token_scale_ptr,
1921
+ valid,
1922
+ NOPE_DIM,
1923
+ BLOCK_K,
1924
+ IS_FNUZ,
1925
+ )
1926
+ if not TRUST_EXTRA_CACHE_NAN_FREE:
1927
+ zero = tl.zeros((BLOCK_K, 128), dtype=tl.bfloat16)
1928
+ k_nope_0a = tl.where(k_nope_0a == k_nope_0a, k_nope_0a, zero)
1929
+ k_nope_0b = tl.where(k_nope_0b == k_nope_0b, k_nope_0b, zero)
1930
+ k_nope_1 = tl.where(k_nope_1 == k_nope_1, k_nope_1, zero)
1931
+ k_tail = tl.where(k_tail == k_tail, k_tail, zero)
1932
+ k_nope_0 = tl.cat(k_nope_0a, k_nope_0b, dim=1)
1933
+ k_tail_256 = tl.cat(k_nope_1, k_tail, dim=1)
1934
+ k_combined = tl.cat(k_nope_0, k_tail_256, dim=1)
1935
+
1936
+ scores = tl.dot(q_combined, tl.trans(k_combined))
1937
+ scores *= scale * 1.4426950408889634
1938
+ scores = tl.where(
1939
+ head_mask[:, None] & valid[None, :],
1940
+ scores,
1941
+ -3.4028234663852886e38,
1942
+ )
1943
+ m_block = tl.max(scores, axis=1)
1944
+ m_new = tl.maximum(m_i, m_block)
1945
+ alpha = tl.exp2(m_i - m_new)
1946
+ p = tl.exp2(scores - m_new[:, None])
1947
+ p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0)
1948
+ l_new = l_i * alpha + tl.sum(p, axis=1)
1949
+ p_bf16 = p.to(k_nope_0a.dtype)
1950
+ acc_nope_0a = acc_nope_0a * alpha[:, None] + tl.dot(p_bf16, k_nope_0a)
1951
+ acc_nope_0b = acc_nope_0b * alpha[:, None] + tl.dot(p_bf16, k_nope_0b)
1952
+ acc_nope_1 = acc_nope_1 * alpha[:, None] + tl.dot(p_bf16, k_nope_1)
1953
+ acc_tail = acc_tail * alpha[:, None] + tl.dot(p_bf16, k_tail)
1954
+ return (
1955
+ m_new,
1956
+ l_new,
1957
+ acc_nope_0a,
1958
+ acc_nope_0b,
1959
+ acc_nope_1,
1960
+ acc_tail,
1961
+ )
1962
+
1963
+
1964
+@triton.jit
1965
+def _sparse_attn_decode_gfx950_partial_kernel(
1966
+ q_ptr,
1967
+ main_cache_ptr,
1968
+ main_indices_ptr,
1969
+ main_indptr_ptr,
1970
+ extra_cache_ptr,
1971
+ extra_indices_ptr,
1972
+ extra_indptr_ptr,
1973
+ part_m_ptr,
1974
+ part_l_ptr,
1975
+ part_acc_ptr,
1976
+ q_stride0: tl.constexpr,
1977
+ q_stride1: tl.constexpr,
1978
+ main_cache_stride0: tl.constexpr,
1979
+ extra_cache_stride0: tl.constexpr,
1980
+ main_num_rows,
1981
+ extra_num_rows,
1982
+ MAIN_BLOCK_SIZE: tl.constexpr,
1983
+ EXTRA_BLOCK_SIZE: tl.constexpr,
1984
+ scale: tl.constexpr,
1985
+ num_heads: tl.constexpr,
1986
+ HAS_EXTRA: tl.constexpr,
1987
+ NOPE_DIM: tl.constexpr,
1988
+ ROPE_DIM: tl.constexpr,
1989
+ IS_FNUZ_MAIN: tl.constexpr,
1990
+ IS_FNUZ_EXTRA: tl.constexpr,
1991
+ TRUST_EXTRA_CACHE_NAN_FREE: tl.constexpr,
1992
+ ADAPTIVE_SPLITS: tl.constexpr,
1993
+ ONE_WAVE_SPLITS: tl.constexpr,
1994
+ BLOCK_H: tl.constexpr,
1995
+ BLOCK_K: tl.constexpr,
1996
+ NUM_SPLITS: tl.constexpr,
1997
+ NUM_STAGES: tl.constexpr,
1998
+):
1999
+ query_idx = tl.program_id(0)
2000
+ split_id = tl.program_id(1)
2001
+ pid_h = tl.program_id(2)
2002
+
2003
+ tl.static_assert(NOPE_DIM == 448)
2004
+ tl.static_assert(ROPE_DIM == 64)
2005
+
2006
+ head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H)
2007
+ if num_heads % BLOCK_H == 0:
2008
+ head_mask = tl.full((BLOCK_H,), True, tl.int1)
2009
+ else:
2010
+ head_mask = head_offsets < num_heads
2011
+ neg_large = -3.4028234663852886e38
2012
+
2013
+ if ADAPTIVE_SPLITS:
2014
+ main_start = tl.load(main_indptr_ptr + query_idx)
2015
+ main_end = tl.load(main_indptr_ptr + query_idx + 1)
2016
+ main_len = main_end - main_start
2017
+ if HAS_EXTRA:
2018
+ extra_start = tl.load(extra_indptr_ptr + query_idx)
2019
+ extra_end = tl.load(extra_indptr_ptr + query_idx + 1)
2020
+ extra_len = extra_end - extra_start
2021
+ else:
2022
+ extra_start = 0
2023
+ extra_len = 0
2024
+ split4_span: tl.constexpr = 4 * BLOCK_K
2025
+ split4_iters = (main_len + split4_span - 1) // split4_span
2026
+ split4_iters += (extra_len + split4_span - 1) // split4_span
2027
+ use_four_splits = split4_iters <= 3
2028
+ work_splits = NUM_SPLITS
2029
+ if ONE_WAVE_SPLITS > 4 and ONE_WAVE_SPLITS < NUM_SPLITS:
2030
+ one_wave_span: tl.constexpr = ONE_WAVE_SPLITS * BLOCK_K
2031
+ one_wave_iters = (main_len + one_wave_span - 1) // one_wave_span
2032
+ one_wave_iters += (extra_len + one_wave_span - 1) // one_wave_span
2033
+ work_splits = tl.where(one_wave_iters <= 3, ONE_WAVE_SPLITS, work_splits)
2034
+ work_splits = tl.where(use_four_splits, 4, work_splits)
2035
+ if split_id >= work_splits:
2036
+ pm_base = (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets
2037
+ tl.store(part_m_ptr + pm_base, neg_large, mask=head_mask)
2038
+ tl.store(part_l_ptr + pm_base, 0.0, mask=head_mask)
2039
+ return
2040
+ else:
2041
+ work_splits = NUM_SPLITS
2042
+
2043
+ nope_offsets_0a = tl.arange(0, 128)
2044
+ nope_offsets_0b = 128 + tl.arange(0, 128)
2045
+ nope_offsets_0 = tl.arange(0, 256)
2046
+ tail_offsets = 256 + tl.arange(0, 256)
2047
+ nope_offsets_1 = 256 + tl.arange(0, 128)
2048
+ tail_offsets_128 = 384 + tl.arange(0, 128)
2049
+
2050
+ q_row_ptr = q_ptr + query_idx * q_stride0 + head_offsets[:, None] * q_stride1
2051
+ q_nope_0 = tl.load(
2052
+ q_row_ptr + nope_offsets_0[None, :],
2053
+ mask=head_mask[:, None],
2054
+ other=0.0,
2055
+ )
2056
+ q_tail = tl.load(
2057
+ q_row_ptr + tail_offsets[None, :],
2058
+ mask=head_mask[:, None],
2059
+ other=0.0,
2060
+ )
2061
+ q_combined = tl.cat(q_nope_0, q_tail, dim=1)
2062
+
2063
+ m_i = tl.full((BLOCK_H,), neg_large, dtype=tl.float32)
2064
+ l_i = tl.zeros((BLOCK_H,), dtype=tl.float32)
2065
+ acc_nope_0a = tl.zeros((BLOCK_H, 128), dtype=tl.float32)
2066
+ acc_nope_0b = tl.zeros((BLOCK_H, 128), dtype=tl.float32)
2067
+ acc_nope_1 = tl.zeros((BLOCK_H, 128), dtype=tl.float32)
2068
+ acc_tail = tl.zeros((BLOCK_H, 128), dtype=tl.float32)
2069
+ k_offsets = tl.arange(0, BLOCK_K)
2070
+
2071
+ if not ADAPTIVE_SPLITS:
2072
+ main_start = tl.load(main_indptr_ptr + query_idx)
2073
+ main_end = tl.load(main_indptr_ptr + query_idx + 1)
2074
+ main_len = main_end - main_start
2075
+ main_chunk = (main_len + work_splits - 1) // work_splits
2076
+ main_lo = split_id * main_chunk
2077
+ main_hi = tl.minimum(main_lo + main_chunk, main_len)
2078
+
2079
+ for k_start in tl.range(
2080
+ main_lo,
2081
+ main_hi,
2082
+ BLOCK_K,
2083
+ num_stages=NUM_STAGES,
2084
+ ):
2085
+ k_pos = k_start + k_offsets
2086
+ in_range = k_pos < main_hi
2087
+ slot = tl.load(main_indices_ptr + main_start + k_pos, mask=in_range, other=-1)
2088
+ valid = in_range & (slot >= 0) & (slot < main_num_rows)
2089
+ (
2090
+ m_i,
2091
+ l_i,
2092
+ acc_nope_0a,
2093
+ acc_nope_0b,
2094
+ acc_nope_1,
2095
+ acc_tail,
2096
+ ) = _sparse_attn_decode_gfx950_partial_loaded_tile(
2097
+ q_combined,
2098
+ main_cache_ptr,
2099
+ slot,
2100
+ valid,
2101
+ main_cache_stride0,
2102
+ scale,
2103
+ head_mask,
2104
+ m_i,
2105
+ l_i,
2106
+ acc_nope_0a,
2107
+ acc_nope_0b,
2108
+ acc_nope_1,
2109
+ acc_tail,
2110
+ MAIN_BLOCK_SIZE,
2111
+ NOPE_DIM,
2112
+ BLOCK_K,
2113
+ IS_FNUZ_MAIN,
2114
+ False,
2115
+ )
2116
+
2117
+ if HAS_EXTRA:
2118
+ if not ADAPTIVE_SPLITS:
2119
+ extra_start = tl.load(extra_indptr_ptr + query_idx)
2120
+ extra_end = tl.load(extra_indptr_ptr + query_idx + 1)
2121
+ extra_len = extra_end - extra_start
2122
+ extra_chunk = (extra_len + work_splits - 1) // work_splits
2123
+ extra_lo = split_id * extra_chunk
2124
+ extra_hi = tl.minimum(extra_lo + extra_chunk, extra_len)
2125
+
2126
+ outer_block_k: tl.constexpr = 2 * BLOCK_K
2127
+ outer_k_offsets = tl.arange(0, outer_block_k)
2128
+ extra_hi_full = (
2129
+ extra_lo + ((extra_hi - extra_lo) // outer_block_k) * outer_block_k
2130
+ )
2131
+ for k_start in tl.range(
2132
+ extra_lo,
2133
+ extra_hi_full,
2134
+ outer_block_k,
2135
+ num_stages=NUM_STAGES,
2136
+ ):
2137
+ slot = tl.load(extra_indices_ptr + extra_start + k_start + outer_k_offsets)
2138
+ valid = (slot >= 0) & (slot < extra_num_rows)
2139
+ slot_pairs = tl.trans(tl.reshape(slot, (2, BLOCK_K)))
2140
+ valid_pairs = tl.trans(tl.reshape(valid, (2, BLOCK_K)))
2141
+ slot_lo, slot_hi = tl.split(slot_pairs)
2142
+ valid_lo, valid_hi = tl.split(valid_pairs)
2143
+ (
2144
+ m_i,
2145
+ l_i,
2146
+ acc_nope_0a,
2147
+ acc_nope_0b,
2148
+ acc_nope_1,
2149
+ acc_tail,
2150
+ ) = _sparse_attn_decode_gfx950_partial_loaded_tile(
2151
+ q_combined,
2152
+ extra_cache_ptr,
2153
+ slot_lo,
2154
+ valid_lo,
2155
+ extra_cache_stride0,
2156
+ scale,
2157
+ head_mask,
2158
+ m_i,
2159
+ l_i,
2160
+ acc_nope_0a,
2161
+ acc_nope_0b,
2162
+ acc_nope_1,
2163
+ acc_tail,
2164
+ EXTRA_BLOCK_SIZE,
2165
+ NOPE_DIM,
2166
+ BLOCK_K,
2167
+ IS_FNUZ_EXTRA,
2168
+ TRUST_EXTRA_CACHE_NAN_FREE,
2169
+ )
2170
+ (
2171
+ m_i,
2172
+ l_i,
2173
+ acc_nope_0a,
2174
+ acc_nope_0b,
2175
+ acc_nope_1,
2176
+ acc_tail,
2177
+ ) = _sparse_attn_decode_gfx950_partial_loaded_tile(
2178
+ q_combined,
2179
+ extra_cache_ptr,
2180
+ slot_hi,
2181
+ valid_hi,
2182
+ extra_cache_stride0,
2183
+ scale,
2184
+ head_mask,
2185
+ m_i,
2186
+ l_i,
2187
+ acc_nope_0a,
2188
+ acc_nope_0b,
2189
+ acc_nope_1,
2190
+ acc_tail,
2191
+ EXTRA_BLOCK_SIZE,
2192
+ NOPE_DIM,
2193
+ BLOCK_K,
2194
+ IS_FNUZ_EXTRA,
2195
+ TRUST_EXTRA_CACHE_NAN_FREE,
2196
+ )
2197
+ for tail_idx in tl.static_range(2):
2198
+ tail_start = extra_hi_full + tail_idx * BLOCK_K
2199
+ if tail_start < extra_hi:
2200
+ k_pos = tail_start + k_offsets
2201
+ in_range = k_pos < extra_hi
2202
+ slot = tl.load(
2203
+ extra_indices_ptr + extra_start + k_pos,
2204
+ mask=in_range,
2205
+ other=-1,
2206
+ )
2207
+ valid = in_range & (slot >= 0) & (slot < extra_num_rows)
2208
+ (
2209
+ m_i,
2210
+ l_i,
2211
+ acc_nope_0a,
2212
+ acc_nope_0b,
2213
+ acc_nope_1,
2214
+ acc_tail,
2215
+ ) = _sparse_attn_decode_gfx950_partial_loaded_tile(
2216
+ q_combined,
2217
+ extra_cache_ptr,
2218
+ slot,
2219
+ valid,
2220
+ extra_cache_stride0,
2221
+ scale,
2222
+ head_mask,
2223
+ m_i,
2224
+ l_i,
2225
+ acc_nope_0a,
2226
+ acc_nope_0b,
2227
+ acc_nope_1,
2228
+ acc_tail,
2229
+ EXTRA_BLOCK_SIZE,
2230
+ NOPE_DIM,
2231
+ BLOCK_K,
2232
+ IS_FNUZ_EXTRA,
2233
+ TRUST_EXTRA_CACHE_NAN_FREE,
2234
+ )
2235
+
2236
+ pm_base = (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets
2237
+ m_store = tl.where(l_i > 0.0, m_i * 0.6931471805599453, neg_large)
2238
+ tl.store(part_m_ptr + pm_base, m_store, mask=head_mask)
2239
+ tl.store(part_l_ptr + pm_base, l_i, mask=head_mask)
2240
+ acc_base = part_acc_ptr + (
2241
+ (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets[:, None]
2242
+ ) * (NOPE_DIM + ROPE_DIM)
2243
+ tl.store(
2244
+ acc_base + nope_offsets_0a[None, :],
2245
+ acc_nope_0a,
2246
+ mask=head_mask[:, None],
2247
+ )
2248
+ tl.store(
2249
+ acc_base + nope_offsets_0b[None, :],
2250
+ acc_nope_0b,
2251
+ mask=head_mask[:, None],
2252
+ )
2253
+ tl.store(
2254
+ acc_base + nope_offsets_1[None, :],
2255
+ acc_nope_1,
2256
+ mask=head_mask[:, None],
2257
+ )
2258
+ tl.store(
2259
+ acc_base + tail_offsets_128[None, :],
2260
+ acc_tail,
2261
+ mask=head_mask[:, None],
2262
+ )
2263
+
2264
+
2265
@triton.jit
2266
def _sparse_attn_decode_reduce_kernel(
2267
part_m_ptr,
@@ -1701,6 +2278,7 @@ def _sparse_attn_decode_reduce_kernel(
2278
pa_stride_h,
2279
num_heads,
2280
HAS_ATTN_SINK: tl.constexpr,
2281
+ ADAPTIVE_SPLITS: tl.constexpr,
2282
COMB_DIM: tl.constexpr,
2283
BLOCK_H: tl.constexpr,
2284
NUM_SPLITS: tl.constexpr,
@@ -1769,17 +2347,27 @@ def _sparse_attn_decode_reduce_kernel(
2347
other=neg_large,
2348
)
2349
w_s = tl.exp(m_s - m_final)
2350
+ if ADAPTIVE_SPLITS:
2351
+ active_split = m_s > neg_large
2352
+ w_s = tl.where(head_mask & active_split, w_s, 0.0)
2353
acc_base = (
2354
part_acc_ptr
2355
+ query_idx * pa_stride0
2356
+ s * pa_stride_s
2357
+ head_offsets[:, None] * pa_stride_h
2358
)
1778
- acc_s = tl.load(
1779
- acc_base + comb_offsets[None, :],
1780
- mask=head_mask[:, None],
1781
- other=0.0,
1782
- )
2359
+ if ADAPTIVE_SPLITS:
2360
+ acc_s = tl.load(
2361
+ acc_base + comb_offsets[None, :],
2362
+ mask=head_mask[:, None] & active_split[:, None],
2363
+ other=0.0,
2364
+ )
2365
+ else:
2366
+ acc_s = tl.load(
2367
+ acc_base + comb_offsets[None, :],
2368
+ mask=head_mask[:, None],
2369
+ other=0.0,
2370
+ )
2371
acc += w_s[:, None] * acc_s
2372
2373
out = tl.where(l_final[:, None] > 0.0, acc / denom[:, None], 0.0)
@@ -1986,6 +2574,49 @@ def _decode_num_splits(
2574
return best_splits
2575
2576
2577
+def _decode_gfx950_num_splits(
2578
+ num_queries: int,
2579
+ heads_blocks: int,
2580
+ avg_main_len: float = 0.0,
2581
+ avg_extra_len: float = 0.0,
2582
+ block_k: int = 32,
2583
+) -> int:
2584
+ base = max(1, num_queries * heads_blocks)
2585
+ cu = max(1, _decode_cu_count())
2586
+ target_workgroups = 2 * cu
2587
+ num_splits = min(
2588
+ 32,
2589
+ max(
2590
+ 1,
2591
+ math.ceil(target_workgroups / base),
2592
+ ),
2593
+ )
2594
+ if (
2595
+ base >= 16
2596
+ and num_splits > 4
2597
+ and _decode_partial_iters(avg_main_len, avg_extra_len, 4, block_k) <= 3
2598
+ ):
2599
+ return 4
2600
+ if 16 <= base < 64:
2601
+ one_wave_splits = max(1, cu // base)
2602
+ one_wave_iters = _decode_partial_iters(
2603
+ avg_main_len, avg_extra_len, one_wave_splits, block_k
2604
+ )
2605
+ target_waves = 1 if one_wave_iters <= 9 else 2
2606
+ num_splits = min(num_splits, max(1, target_waves * cu // base))
2607
+ if base >= 16 and num_splits > 1:
2608
+ target_waves = (base * num_splits + cu - 1) // cu
2609
+ target_iters = _decode_partial_iters(
2610
+ avg_main_len, avg_extra_len, num_splits, block_k
2611
+ )
2612
+ for splits in range(1, num_splits):
2613
+ waves = (base * splits + cu - 1) // cu
2614
+ iters = _decode_partial_iters(avg_main_len, avg_extra_len, splits, block_k)
2615
+ if waves == target_waves and iters == target_iters:
2616
+ return splits
2617
+ return num_splits
2618
+
2619
+
2620
def _rocm_sparse_attn_decode_ragged_triton(
2621
q: torch.Tensor,
2622
main_cache: torch.Tensor,
@@ -1998,6 +2629,9 @@ def _rocm_sparse_attn_decode_ragged_triton(
2629
extra_cache: torch.Tensor | None = None,
2630
extra_indices: torch.Tensor | None = None,
2631
extra_indptr: torch.Tensor | None = None,
2632
+ out: torch.Tensor | None = None,
2633
+ extra_cache_nan_free: bool = False,
2634
+ adaptive_splits: bool = False,
2635
) -> torch.Tensor:
2636
assert q.ndim == 3, f"expected q=[b,h,d], got {q.shape}"
2637
assert main_cache.ndim == 3, (
@@ -2038,6 +2672,10 @@ def _rocm_sparse_attn_decode_ragged_triton(
2672
and extra_indices is not None
2673
and extra_indptr is not None
2674
)
2675
+ assert not extra_cache_nan_free or (_ON_GFX950 and has_extra), (
2676
+ "extra_cache_nan_free requires a gfx950 compressed cache with trusted "
2677
+ "canonical-writer provenance"
2678
+ )
2679
if has_extra:
2680
assert extra_cache is not None
2681
assert extra_indices is not None
@@ -2059,7 +2697,16 @@ def _rocm_sparse_attn_decode_ragged_triton(
2697
extra_indptr = torch.zeros(num_queries + 1, device=q.device, dtype=torch.int32)
2698
2699
block_h = 16
2062
- out = torch.empty_like(q, dtype=torch.bfloat16)
2700
+ if out is None:
2701
+ out = torch.empty_like(q, dtype=torch.bfloat16)
2702
+ else:
2703
+ assert out.shape == q.shape, f"expected out shape {q.shape}, got {out.shape}"
2704
+ assert out.device == q.device, (
2705
+ f"expected out on device {q.device}, got {out.device}"
2706
+ )
2707
+ assert out.dtype == torch.bfloat16, (
2708
+ f"expected out dtype {torch.bfloat16}, got {out.dtype}"
2709
+ )
2710
heads_blocks = triton.cdiv(num_heads, block_h)
2711
nope_block = triton.next_power_of_2(nope_head_dim)
2712
comb_dim = nope_head_dim + rope_head_dim
@@ -2103,14 +2750,35 @@ def _rocm_sparse_attn_decode_ragged_triton(
2750
return out
2751
2752
block_k = 32 # KV tokens walked per split-K iteration. Tuned on gfx950.
2106
- # Average per-query segment lengths, read sync-free from the ragged index
2107
- # sizes, let the split heuristic avoid over-splitting
2108
- # main_indices/extra_indices are flat [nnz] int32.
2109
- inv_q = 1.0 / max(1, num_queries)
2110
- avg_main_len = main_indices.numel() * inv_q
2111
- avg_extra_len = (extra_indices.numel() * inv_q) if has_extra else 0.0
2112
- num_splits = _decode_num_splits(
2113
- num_queries, heads_blocks, avg_main_len, avg_extra_len, block_k
2753
+ if _ON_GFX950:
2754
+ inv_q = 1.0 / max(1, num_queries)
2755
+ avg_main_len = main_indices.numel() * inv_q
2756
+ avg_extra_len = (extra_indices.numel() * inv_q) if has_extra else 0.0
2757
+ num_splits = _decode_gfx950_num_splits(
2758
+ num_queries,
2759
+ heads_blocks,
2760
+ avg_main_len,
2761
+ avg_extra_len,
2762
+ block_k,
2763
+ )
2764
+ else:
2765
+ # Average per-query segment lengths, read sync-free from the ragged
2766
+ # index sizes, let the split heuristic avoid over-splitting.
2767
+ inv_q = 1.0 / max(1, num_queries)
2768
+ avg_main_len = main_indices.numel() * inv_q
2769
+ avg_extra_len = (extra_indices.numel() * inv_q) if has_extra else 0.0
2770
+ num_splits = _decode_num_splits(
2771
+ num_queries, heads_blocks, avg_main_len, avg_extra_len, block_k
2772
+ )
2773
+
2774
+ base_workgroups = num_queries * heads_blocks
2775
+ adaptive_splits = (
2776
+ _ON_GFX950 and adaptive_splits and base_workgroups >= 16 and num_splits > 4
2777
+ )
2778
+ one_wave_splits = (
2779
+ max(1, _decode_cu_count() // base_workgroups)
2780
+ if adaptive_splits and 16 <= base_workgroups < 64
2781
+ else num_splits
2782
)
2783
2784
part_m = torch.empty(
@@ -2123,48 +2791,88 @@ def _rocm_sparse_attn_decode_ragged_triton(
2791
device=q.device,
2792
)
2793
2126
- _sparse_attn_decode_partial_kernel[(num_queries, num_splits, heads_blocks)](
2127
- q,
2128
- main_cache,
2129
- main_indices,
2130
- main_indptr,
2131
- extra_cache,
2132
- extra_indices,
2133
- extra_indptr,
2134
- part_m,
2135
- part_l,
2136
- part_acc,
2137
- q.stride(0),
2138
- q.stride(1),
2139
- main_cache.stride(0),
2140
- extra_cache.stride(0),
2141
- part_m.stride(0),
2142
- part_m.stride(1),
2143
- part_acc.stride(0),
2144
- part_acc.stride(1),
2145
- part_acc.stride(2),
2146
- main_cache.shape[0] * main_cache.shape[1],
2147
- extra_cache.shape[0] * extra_cache.shape[1],
2148
- main_cache.shape[1],
2149
- extra_cache.shape[1],
2150
- scale,
2151
- num_heads,
2152
- HAS_EXTRA=has_extra,
2153
- NOPE_DIM=nope_head_dim,
2154
- NOPE_BLOCK=nope_block,
2155
- ROPE_DIM=rope_head_dim,
2156
- # main_cache = swa_k_cache (C++ encoder, FNUZ on gfx942 / OCP on gfx950).
2157
- # extra_cache = compressed kv_cache (Triton encoder, OCP everywhere).
2158
- # Reading both with a single IS_FNUZ would decode one of them with the
2159
- # wrong FNUZ/OCP scale ratio (~1.87×).
2160
- IS_FNUZ_MAIN=is_fnuz,
2161
- IS_FNUZ_EXTRA=False,
2162
- BLOCK_H=block_h,
2163
- BLOCK_K=block_k,
2164
- NUM_SPLITS=num_splits,
2165
- NUM_STAGES=1,
2166
- num_warps=4,
2167
- )
2794
+ if _ON_GFX950:
2795
+ _sparse_attn_decode_gfx950_partial_kernel[
2796
+ (num_queries, num_splits, heads_blocks)
2797
+ ](
2798
+ q,
2799
+ main_cache,
2800
+ main_indices,
2801
+ main_indptr,
2802
+ extra_cache,
2803
+ extra_indices,
2804
+ extra_indptr,
2805
+ part_m,
2806
+ part_l,
2807
+ part_acc,
2808
+ q.stride(0),
2809
+ q.stride(1),
2810
+ main_cache.stride(0),
2811
+ extra_cache.stride(0),
2812
+ main_cache.shape[0] * main_cache.shape[1],
2813
+ extra_cache.shape[0] * extra_cache.shape[1],
2814
+ main_cache.shape[1],
2815
+ extra_cache.shape[1],
2816
+ scale,
2817
+ num_heads,
2818
+ HAS_EXTRA=has_extra,
2819
+ NOPE_DIM=nope_head_dim,
2820
+ ROPE_DIM=rope_head_dim,
2821
+ IS_FNUZ_MAIN=is_fnuz,
2822
+ IS_FNUZ_EXTRA=False,
2823
+ TRUST_EXTRA_CACHE_NAN_FREE=extra_cache_nan_free,
2824
+ ADAPTIVE_SPLITS=adaptive_splits,
2825
+ ONE_WAVE_SPLITS=one_wave_splits,
2826
+ BLOCK_H=block_h,
2827
+ BLOCK_K=block_k,
2828
+ NUM_SPLITS=num_splits,
2829
+ NUM_STAGES=1,
2830
+ num_warps=4,
2831
+ waves_per_eu=0,
2832
+ )
2833
+ else:
2834
+ _sparse_attn_decode_partial_kernel[(num_queries, num_splits, heads_blocks)](
2835
+ q,
2836
+ main_cache,
2837
+ main_indices,
2838
+ main_indptr,
2839
+ extra_cache,
2840
+ extra_indices,
2841
+ extra_indptr,
2842
+ part_m,
2843
+ part_l,
2844
+ part_acc,
2845
+ q.stride(0),
2846
+ q.stride(1),
2847
+ main_cache.stride(0),
2848
+ extra_cache.stride(0),
2849
+ part_m.stride(0),
2850
+ part_m.stride(1),
2851
+ part_acc.stride(0),
2852
+ part_acc.stride(1),
2853
+ part_acc.stride(2),
2854
+ main_cache.shape[0] * main_cache.shape[1],
2855
+ extra_cache.shape[0] * extra_cache.shape[1],
2856
+ main_cache.shape[1],
2857
+ extra_cache.shape[1],
2858
+ scale,
2859
+ num_heads,
2860
+ HAS_EXTRA=has_extra,
2861
+ NOPE_DIM=nope_head_dim,
2862
+ NOPE_BLOCK=nope_block,
2863
+ ROPE_DIM=rope_head_dim,
2864
+ # main_cache = swa_k_cache (C++ encoder, FNUZ on gfx942 / OCP on gfx950).
2865
+ # extra_cache = compressed kv_cache (Triton encoder, OCP everywhere).
2866
+ # Reading both with a single IS_FNUZ would decode one of them with the
2867
+ # wrong FNUZ/OCP scale ratio (~1.87×).
2868
+ IS_FNUZ_MAIN=is_fnuz,
2869
+ IS_FNUZ_EXTRA=False,
2870
+ BLOCK_H=block_h,
2871
+ BLOCK_K=block_k,
2872
+ NUM_SPLITS=num_splits,
2873
+ NUM_STAGES=1,
2874
+ num_warps=4,
2875
+ )
2876
2877
_sparse_attn_decode_reduce_kernel[(num_queries, num_heads)](
2878
part_m,
@@ -2181,6 +2889,7 @@ def _rocm_sparse_attn_decode_ragged_triton(
2889
part_acc.stride(2),
2890
num_heads,
2891
HAS_ATTN_SINK=has_attn_sink,
2892
+ ADAPTIVE_SPLITS=adaptive_splits,
2893
COMB_DIM=comb_dim,
2894
BLOCK_H=1,
2895
NUM_SPLITS=num_splits,
@@ -2206,6 +2915,9 @@ def _rocm_sparse_attn_decode_triton(
2915
main_ragged_indptr: torch.Tensor | None = None,
2916
extra_ragged_indices: torch.Tensor | None = None,
2917
extra_ragged_indptr: torch.Tensor | None = None,
2918
+ out: torch.Tensor | None = None,
2919
+ extra_cache_nan_free: bool = False,
2920
+ adaptive_splits: bool = False,
2921
) -> torch.Tensor:
2922
if main_ragged_indices is None or main_ragged_indptr is None:
2923
main_ragged_indices, main_ragged_indptr = build_ragged_indices_from_dense(
@@ -2241,6 +2953,9 @@ def _rocm_sparse_attn_decode_triton(
2953
extra_cache=extra_cache,
2954
extra_indices=extra_ragged_indices,
2955
extra_indptr=extra_ragged_indptr,
2956
+ out=out,
2957
+ extra_cache_nan_free=extra_cache_nan_free,
2958
+ adaptive_splits=adaptive_splits,
2959
)
2960
2961
@@ -2312,6 +3027,8 @@ def rocm_sparse_attn_decode(
3027
nope_head_dim: int,
3028
rope_head_dim: int,
3029
output: torch.Tensor,
3030
+ extra_cache_nan_free: bool = False,
3031
+ adaptive_splits: bool = False,
3032
) -> None:
3033
assert swa_k_cache.dtype == torch.uint8, (
3034
"ROCm Triton sparse decode expects uint8 fp8_ds_mla SWA cache, "
@@ -2341,6 +3058,7 @@ def rocm_sparse_attn_decode(
3058
if topk_indices is not None:
3059
extra_indices = topk_indices.reshape(topk_indices.shape[0], -1)
3060
3061
+ direct_out = output if _ON_GFX950 and output.dtype == torch.bfloat16 else None
3062
attn_out = _rocm_sparse_attn_decode_triton(
3063
q=q,
3064
main_cache=swa_k_cache,
@@ -2357,5 +3075,9 @@ def rocm_sparse_attn_decode(
3075
main_ragged_indptr=swa_ragged_indptr,
3076
extra_ragged_indices=topk_ragged_indices,
3077
extra_ragged_indptr=topk_ragged_indptr,
3078
+ out=direct_out,
3079
+ extra_cache_nan_free=extra_cache_nan_free,
3080
+ adaptive_splits=adaptive_splits,
3081
)
2361
- output.copy_(attn_out.to(output.dtype))
3082
+ if direct_out is None:
3083
+ output.copy_(attn_out.to(output.dtype))