[Attention] Add FlashInfer XQA decode support on SM12x (#49718)
Signed-off-by: Andrii Skliar <askliar@nvidia.com> Signed-off-by: Andrii Skliar <andreyws96@gmail.com> Co-authored-by: Andrii Skliar <askliar@nvidia.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Andrii Skliar committed
Aug 12, 2026 at 01:32 UTC
1d2d83a07fd3180068917a031c28dcc83141d0be
4 files changed
+413
-57
tests/kernels/attention/test_use_trtllm_attention.py
+14
@@ -101,6 +101,20 @@ def test_supports_sm90_decode_only(_art, _family, _cap):
101
assert supports_trtllm_attention(is_prefill=True) is False
102
103
104
+@patch("vllm.envs.VLLM_BATCH_INVARIANT", False)
105
+@patch(
106
+ "vllm.utils.flashinfer.current_platform.is_device_capability", return_value=False
107
+)
108
+@patch(
109
+ "vllm.utils.flashinfer.current_platform.is_device_capability_family",
110
+ side_effect=lambda capability: capability == 120,
111
+)
112
+@patch("vllm.utils.flashinfer.has_nvidia_artifactory", return_value=True)
113
+def test_supports_sm12x_decode_only(_art, _family, _cap):
114
+ assert supports_trtllm_attention(is_prefill=False) is True
115
+ assert supports_trtllm_attention(is_prefill=True) is False
116
+
117
+
118
@patch("vllm.envs.VLLM_BATCH_INVARIANT", False)
119
@patch(
120
"vllm.utils.flashinfer.current_platform.is_device_capability_family",
tests/v1/attention/test_attention_backends.py
+171
-17
@@ -250,6 +250,7 @@ def run_attention_backend(
250
attn_type: AttentionType = AttentionType.DECODER,
251
sliding_window: int | None = None,
252
kv_cache_dtype: str = "auto",
253
+ sinks: torch.Tensor | None = None,
254
) -> torch.Tensor:
255
"""Run attention computation using the specified backend's AttentionImpl."""
256
@@ -277,6 +278,7 @@ def run_attention_backend(
278
window_left=-1, # No sliding window
279
logits_soft_cap=0.0, # No soft cap
280
sm_scale=1.0 / (head_size**0.5), # Standard scale
281
+ has_sinks=sinks is not None,
282
)
283
for layer_name in layer_names
284
}
@@ -318,6 +320,7 @@ def run_attention_backend(
320
sliding_window=sliding_window,
321
attn_type=attn_type,
322
kv_cache_dtype=kv_cache_dtype,
323
+ **({"sinks": sinks} if sinks is not None else {}),
324
)
325
326
# Create mock layer and output buffer
@@ -354,10 +357,11 @@ def _test_backend_correctness(
357
rtol: float = 1e-2,
358
tensor_parallel_size: int = 1,
359
kv_cache_dtype: str = "auto",
360
+ use_sinks: bool = False,
361
):
362
"""
363
Test that all backends produce similar outputs to a reference implementation
360
- using torch.nn.functional.scaled_dot_product_attention.
364
+ using FlexAttention or an explicit attention-sink reference.
365
366
This test works by:
367
1. Generating a batch of sequences with specified context and query lengths.
@@ -417,6 +421,11 @@ def _test_backend_correctness(
421
num_kv_heads = vllm_config.model_config.get_num_kv_heads(
422
vllm_config.parallel_config
423
)
424
+ sinks = (
425
+ torch.linspace(-1.0, 1.0, num_q_heads, dtype=torch.float32, device=device)
426
+ if use_sinks
427
+ else None
428
+ )
429
head_size = vllm_config.model_config.get_head_size()
430
sliding_window = vllm_config.model_config.get_sliding_window()
431
dtype = _convert_dtype_to_torch(vllm_config.model_config.dtype)
@@ -471,17 +480,38 @@ def _test_backend_correctness(
480
kv_len = s_len
481
482
final_mask_mod = partial(mask_mod, context_len=context_len)
474
- block_mask = create_block_mask(
475
- final_mask_mod, B=None, H=None, Q_LEN=q_len, KV_LEN=kv_len, device=device
476
- )
477
- sdpa_out_i = flex_attention(
478
- q_sdpa_in,
479
- k_sdpa_in,
480
- v_sdpa_in,
481
- block_mask=block_mask,
482
- scale=scale,
483
- enable_gqa=True,
484
- )
483
+ if sinks is None:
484
+ block_mask = create_block_mask(
485
+ final_mask_mod,
486
+ B=None,
487
+ H=None,
488
+ Q_LEN=q_len,
489
+ KV_LEN=kv_len,
490
+ device=device,
491
+ )
492
+ sdpa_out_i = flex_attention(
493
+ q_sdpa_in,
494
+ k_sdpa_in,
495
+ v_sdpa_in,
496
+ block_mask=block_mask,
497
+ scale=scale,
498
+ enable_gqa=True,
499
+ )
500
+ else:
501
+ scores = (
502
+ torch.matmul(q_sdpa_in.float(), k_sdpa_in.float().transpose(-2, -1))
503
+ * scale
504
+ )
505
+ q_idx = torch.arange(q_len, device=device).unsqueeze(1)
506
+ kv_idx = torch.arange(kv_len, device=device).unsqueeze(0)
507
+ zero = torch.zeros((), dtype=torch.int32, device=device)
508
+ valid = final_mask_mod(zero, zero, q_idx, kv_idx)
509
+ scores.masked_fill_(~valid.unsqueeze(0).unsqueeze(0), -torch.inf)
510
+ sink_logits = sinks.view(1, num_q_heads, 1, 1).expand(1, -1, q_len, -1)
511
+ weights = torch.softmax(torch.cat((scores, sink_logits), dim=-1), dim=-1)[
512
+ ..., :kv_len
513
+ ]
514
+ sdpa_out_i = torch.matmul(weights, v_sdpa_in.float()).to(dtype)
515
516
all_sdpa_outputs.append(sdpa_out_i.transpose(1, 2).squeeze(0))
517
@@ -574,6 +604,7 @@ def _test_backend_correctness(
604
sliding_window=sliding_window,
605
attn_type=attn_type,
606
kv_cache_dtype=kv_cache_dtype,
607
+ sinks=sinks,
608
)
609
finally:
610
if reset_kv_cache_layout:
@@ -706,6 +737,86 @@ def test_flashinfer_xqa_bmm1_scale_matches_decode_q_dtype():
737
assert impl.get_xqa_bmm1_scale(MockLayer, torch.float8_e4m3fn) == 3.0
738
739
740
+@pytest.mark.skipif(
741
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
742
+ reason="FlashInfer is not available.",
743
+)
744
+def test_flashinfer_xqa_draft_masks():
745
+ from vllm.v1.attention.backends import flashinfer as flashinfer_backend
746
+
747
+ device = torch.device("cpu")
748
+ causal = flashinfer_backend._make_xqa_draft_block_mask(3, True, device)
749
+ full = flashinfer_backend._make_xqa_draft_block_mask(3, False, device)
750
+ ragged = flashinfer_backend._make_xqa_ragged_draft_block_mask(
751
+ [2, 3], 3, True, device
752
+ )
753
+
754
+ assert torch.equal(
755
+ causal.view(torch.int16),
756
+ torch.tensor([[1, 0], [3, 0], [7, 0]], dtype=torch.int16),
757
+ )
758
+ assert torch.equal(
759
+ full.view(torch.int16), torch.tensor([[7, 0]] * 3, dtype=torch.int16)
760
+ )
761
+ assert torch.equal(
762
+ ragged.view(torch.int16),
763
+ torch.tensor(
764
+ [[1, 0], [3, 0], [1, 0], [3, 0], [7, 0]],
765
+ dtype=torch.int16,
766
+ ),
767
+ )
768
+
769
+
770
+@pytest.mark.skipif(
771
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
772
+ reason="FlashInfer is not available.",
773
+)
774
+def test_flashinfer_xqa_query_lens_preserve_cudagraph_padding():
775
+ """CUDA-graph padding stays as zero-length requests in ragged offsets."""
776
+ from vllm.v1.attention.backends import flashinfer as flashinfer_backend
777
+
778
+ device = torch.device("cpu")
779
+ builder = object.__new__(flashinfer_backend.FlashInferMetadataBuilder)
780
+ builder.use_dedicated_xqa = True
781
+ qo_indptr = torch.tensor([0, 3, 9, 15, 15], dtype=torch.int32, device=device)
782
+
783
+ q_len, q_cu_seq_lens, q_lens = builder._compute_decode_query_lens(
784
+ qo_indptr,
785
+ qo_indptr,
786
+ num_decodes=4,
787
+ num_decode_tokens=21,
788
+ )
789
+
790
+ assert q_len == 6
791
+ assert q_lens == [3, 6, 6, 0]
792
+ assert q_cu_seq_lens is not None
793
+ assert q_cu_seq_lens.tolist() == [0, 3, 9, 15, 15]
794
+
795
+
796
+@pytest.mark.skipif(
797
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
798
+ reason="FlashInfer is not available.",
799
+)
800
+def test_flashinfer_xqa_query_lens_require_exact_uniform_product():
801
+ from vllm.v1.attention.backends import flashinfer as flashinfer_backend
802
+
803
+ builder = object.__new__(flashinfer_backend.FlashInferMetadataBuilder)
804
+ builder.use_dedicated_xqa = True
805
+ qo_indptr = torch.tensor([0, 3, 3, 3], dtype=torch.int32)
806
+
807
+ q_len, q_cu_seq_lens, q_lens = builder._compute_decode_query_lens(
808
+ qo_indptr,
809
+ qo_indptr,
810
+ num_decodes=3,
811
+ num_decode_tokens=6,
812
+ )
813
+
814
+ assert q_len == 3
815
+ assert q_lens == [3, 0, 0]
816
+ assert q_cu_seq_lens is not None
817
+ assert q_cu_seq_lens.tolist() == [0, 3, 3, 3]
818
+
819
+
820
@pytest.mark.skipif(
821
AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
822
reason="FlashInfer is not available.",
@@ -737,10 +848,48 @@ def test_flashinfer_attention_sinks_refreshed_after_reload(dtype):
848
AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
849
reason="FlashInfer is not available.",
850
)
740
-def test_flashinfer_sm90_xqa_decode_correctness(default_vllm_config):
741
- """FlashInfer should route Hopper decode through XQA and match SDPA."""
742
- if not current_platform.is_cuda() or not current_platform.is_device_capability(90):
743
- pytest.skip("FlashInfer XQA decode requires SM90.")
851
+def test_flashinfer_native_prefill_with_sinks():
852
+ if not (
853
+ current_platform.is_cuda() and current_platform.is_device_capability_family(120)
854
+ ):
855
+ pytest.skip("Native FlashInfer prefill with sinks requires SM12x.")
856
+
857
+ from vllm.v1.attention.backends.flashinfer import FlashInferBackend
858
+
859
+ if not FlashInferBackend.supports_sink():
860
+ pytest.skip("FlashInfer attention sinks are not available in this setup.")
861
+
862
+ def causal_mask_mod(
863
+ b: torch.Tensor,
864
+ h: torch.Tensor,
865
+ q_idx: torch.Tensor,
866
+ kv_idx: torch.Tensor,
867
+ *,
868
+ context_len: int,
869
+ ):
870
+ return (q_idx + context_len) >= kv_idx
871
+
872
+ _test_backend_correctness(
873
+ BATCH_SPECS["small_prefill"],
874
+ "meta-llama/Meta-Llama-3-8B",
875
+ [AttentionBackendEnum.FLASHINFER],
876
+ causal_mask_mod,
877
+ use_sinks=True,
878
+ )
879
+
880
+
881
+@pytest.mark.skipif(
882
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
883
+ reason="FlashInfer is not available.",
884
+)
885
+def test_flashinfer_xqa_decode_correctness(default_vllm_config):
886
+ """FlashInfer should route supported decode through XQA and match SDPA."""
887
+ supported = current_platform.is_cuda() and (
888
+ current_platform.is_device_capability(90)
889
+ or current_platform.is_device_capability_family(120)
890
+ )
891
+ if not supported:
892
+ pytest.skip("FlashInfer XQA decode requires SM90 or SM12x.")
893
894
import unittest.mock
895
@@ -805,11 +954,16 @@ def test_flashinfer_sm90_xqa_decode_correctness(default_vllm_config):
954
)
955
attn_metadata = builder.build(0, common_attn_metadata)
956
957
+ expected_cg_support = (
958
+ AttentionCGSupport.UNIFORM_BATCH
959
+ if current_platform.is_device_capability_family(120)
960
+ else AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
961
+ )
962
assert (
963
flashinfer_backend.FlashInferMetadataBuilder.get_cudagraph_support(
964
vllm_config, kv_cache_spec
965
)
812
- == AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
966
+ == expected_cg_support
967
)
968
assert isinstance(
969
attn_metadata.decode,
vllm/utils/flashinfer.py
+16
-9
@@ -158,6 +158,10 @@ flashinfer_trtllm_batch_decode_sparse_mla_dsv4 = _lazy_import_wrapper(
158
"trtllm_batch_decode_sparse_mla_dsv4",
159
fallback_fn=_missing_sparse_mla,
160
)
161
+flashinfer_xqa_batch_decode_with_kv_cache = _lazy_import_wrapper(
162
+ "flashinfer.decode",
163
+ "xqa_batch_decode_with_kv_cache",
164
+)
165
166
167
# Special case for autotune since it returns a context manager
@@ -375,8 +379,8 @@ def supports_trtllm_attention(is_prefill: bool = False) -> bool:
379
"""Return whether TRTLLM attention is available on the current platform
380
for the given attention phase.
381
378
- SM90 (Hopper) supports the XQA decode kernel but not TRTLLM prefill.
379
- SM100+ supports TRTLLM for both phases. All others are unsupported.
382
+ SM90 (Hopper) and SM12x support the XQA decode kernel but not TRTLLM
383
+ prefill. SM100+ supports TRTLLM for both phases. All others are unsupported.
384
"""
385
# Batch-invariant mode disables TRTLLM attention
386
if envs.VLLM_BATCH_INVARIANT:
@@ -386,8 +390,10 @@ def supports_trtllm_attention(is_prefill: bool = False) -> bool:
390
if not has_nvidia_artifactory():
391
return False
392
389
- # SM90 has XQA decode; prefill is not supported.
390
- if current_platform.is_device_capability(90):
393
+ # SM90 and SM12x have XQA decode only.
394
+ if current_platform.is_device_capability(
395
+ 90
396
+ ) or current_platform.is_device_capability_family(120):
397
return not is_prefill
398
399
# SM100/SM103 has both prefill and decode TRTLLM kernels.
@@ -490,11 +496,11 @@ def use_trtllm_attention(
496
if is_prefill:
497
# Prefill auto-detection
498
use_trtllm = kv_cache_dtype == "auto"
493
- elif current_platform.is_device_capability(90) and kv_cache_dtype.startswith(
494
- "fp8"
495
- ):
496
- # SM90 + FP8 KV cache: prefer the XQA decode kernel. XQA does not
497
- # support NVFP4 KV (that is an SM100 trtllm-gen path only).
499
+ elif (
500
+ current_platform.is_device_capability(90)
501
+ or current_platform.is_device_capability_family(120)
502
+ ) and kv_cache_dtype.startswith("fp8"):
503
+ # SM90/SM12x + FP8 KV cache: prefer the XQA decode kernel.
504
use_trtllm = True
505
else:
506
# Decode auto-detection
@@ -1036,6 +1042,7 @@ __all__ = [
1042
"trtllm_fp4_block_scale_moe",
1043
"flashinfer_trtllm_batch_decode_with_kv_cache_mla",
1044
"flashinfer_trtllm_batch_decode_sparse_mla_dsv4",
1045
+ "flashinfer_xqa_batch_decode_with_kv_cache",
1046
"autotune",
1047
"has_flashinfer_moe",
1048
"has_flashinfer_comm",
vllm/v1/attention/backends/flashinfer.py
+212
-31
@@ -40,6 +40,7 @@ from vllm.platforms.interface import DeviceCapability
40
from vllm.triton_utils import tl, triton
41
from vllm.utils.flashinfer import (
42
can_use_trtllm_attention,
43
+ flashinfer_xqa_batch_decode_with_kv_cache,
44
force_use_trtllm_attention,
45
supports_trtllm_attention,
46
use_trtllm_attention,
@@ -102,6 +103,50 @@ def _get_trtllm_workspace_buffer():
103
return trtllm_workspace_buffer
104
105
106
+def _pack_draft_block_bool_mask(
107
+ bool_mask: torch.Tensor, num_packed: int
108
+) -> torch.Tensor:
109
+ """Pack a boolean draft mask into XQA's uint16 layout."""
110
+ num_rows = bool_mask.shape[0]
111
+ bits = 1 << torch.arange(32, device=bool_mask.device, dtype=torch.int64)
112
+ mask_u32 = (
113
+ (bool_mask.view(num_rows, num_packed, 32).to(torch.int64) * bits)
114
+ .sum(dim=-1)
115
+ .to(torch.uint32)
116
+ )
117
+ return mask_u32.view(torch.uint16).reshape(num_rows, num_packed * 2)
118
+
119
+
120
+def _make_xqa_draft_block_mask(
121
+ q_len: int, causal: bool, device: torch.device
122
+) -> torch.Tensor:
123
+ """Build a packed XQA draft mask for a uniform request."""
124
+ num_packed = (q_len + 31) // 32
125
+ padded = num_packed * 32
126
+ q_idx = torch.arange(q_len, device=device).unsqueeze(1)
127
+ kv_idx = torch.arange(padded, device=device).unsqueeze(0)
128
+ bool_mask = kv_idx <= q_idx if causal else (kv_idx < q_len).expand(q_len, padded)
129
+ return _pack_draft_block_bool_mask(bool_mask, num_packed)
130
+
131
+
132
+def _make_xqa_ragged_draft_block_mask(
133
+ q_lens: list[int], max_q_len: int, causal: bool, device: torch.device
134
+) -> torch.Tensor:
135
+ """Build a packed XQA draft mask for a ragged batch."""
136
+ num_packed = (max_q_len + 31) // 32
137
+ padded = num_packed * 32
138
+ q_lens_t = torch.tensor(q_lens, device=device)
139
+ row_lens = torch.repeat_interleave(q_lens_t, q_lens_t)
140
+ request_starts = torch.cumsum(q_lens_t, dim=0) - q_lens_t
141
+ row_starts = torch.repeat_interleave(request_starts, q_lens_t)
142
+ q_idx = torch.arange(sum(q_lens), device=device) - row_starts
143
+ kv_idx = torch.arange(padded, device=device).unsqueeze(0)
144
+ bool_mask = kv_idx < row_lens.unsqueeze(1)
145
+ if causal:
146
+ bool_mask &= kv_idx <= q_idx.unsqueeze(1)
147
+ return _pack_draft_block_bool_mask(bool_mask, num_packed)
148
+
149
+
150
@triton.jit
151
def _trtllm_prefill_attn_kvfp8_dequant(
152
kv_cache_ptr,
@@ -472,20 +517,20 @@ class FlashInferBackend(AttentionBackend):
517
518
@classmethod
519
def supports_sink(cls) -> bool:
475
- """FlashInfer supports sinks only on the SM100 trtllm-gen path."""
520
+ """FlashInfer supports sinks on SM12x XQA and SM100 trtllm-gen."""
521
from vllm.utils.flashinfer import (
522
force_use_trtllm_attention,
523
)
524
480
- # Respect explicit disable flag (e.g.,
481
- # --attention-config.use_trtllm_attention=0)
525
if force_use_trtllm_attention() is False:
526
return False
527
528
+ if current_platform.is_device_capability_family(120):
529
+ return supports_trtllm_attention(is_prefill=False)
530
+
531
if not current_platform.is_device_capability_family(100):
532
return False
533
488
- # Check if TRTLLM is supported on this platform
534
return supports_trtllm_attention(
535
is_prefill=False
536
) and supports_trtllm_attention(is_prefill=True)
@@ -551,12 +596,7 @@ class TRTLLMPrefill:
596
597
@dataclass
598
class FlashInferTrtllmAPIDecode:
554
- """Metadata for decode paths using FlashInfer's TRTLLM decode API.
555
-
556
- FlashInfer exposes both XQA (SM90) and trtllm-gen (SM100) through
557
- ``trtllm_batch_decode_with_kv_cache``. Keep them as distinct vLLM
558
- decode kernels because their dtype/layout/output constraints differ.
559
- """
599
+ """Metadata for XQA and trtllm-gen decode."""
600
601
kernel: FlashInferDecodeKernel
602
@@ -575,6 +615,15 @@ class FlashInferTrtllmAPIDecode:
615
max_seq_len: int
616
"""The maximum sequence length for KV Cache."""
617
618
+ q_len_per_req: int = 1
619
+ """Query tokens per request for uniform speculative decode."""
620
+
621
+ q_cu_seq_lens: torch.Tensor | None = None
622
+ """Cumulative query lengths for ragged XQA decode."""
623
+
624
+ mask: torch.Tensor | None = None
625
+ """Packed XQA draft mask."""
626
+
627
628
@dataclass
629
class FlashInferMetadata:
@@ -659,7 +708,10 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
708
self.model_config.max_model_len, self.kv_cache_spec.block_size
709
)
710
max_num_reqs = vllm_config.scheduler_config.max_num_seqs
711
+ self.max_num_reqs = max_num_reqs
712
max_num_pages = max_num_reqs * max_num_pages_per_req
713
+ # Persistent uniform masks keep stable addresses for CUDA graphs.
714
+ self._decode_mask_cache: dict[tuple[int, bool], torch.Tensor] = {}
715
speculative_config = vllm_config.speculative_config
716
num_spec_tokens = (
717
speculative_config.num_speculative_tokens
@@ -771,9 +823,14 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
823
)
824
self.use_trtllm_decode_attention = False
825
self.flashinfer_trtllm_api_decode_kernel = None
826
+ self.use_dedicated_xqa = (
827
+ current_platform.is_device_capability_family(120)
828
+ and self.flashinfer_trtllm_api_decode_kernel == FlashInferDecodeKernel.XQA
829
+ )
830
supports_spec_as_decode = (
831
self.flashinfer_trtllm_api_decode_kernel
832
== FlashInferDecodeKernel.TRTLLM_GEN
833
+ or self.use_dedicated_xqa
834
)
835
self._init_reorder_batch_threshold(
836
1,
@@ -856,10 +913,12 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
913
# if cache_config requests a quantized dtype globally.
914
cache_dtype = self.cache_dtype
915
859
- # On SM90, XQA decode requires BF16/FP16-Q even with FP8 KV cache.
860
- # FI native prefill on SM90 still uses FP8-Q in that case.
916
+ # XQA decode requires BF16/FP16-Q even with FP8 KV cache.
917
if (
862
- current_platform.is_device_capability(90)
918
+ (
919
+ current_platform.is_device_capability(90)
920
+ or current_platform.is_device_capability_family(120)
921
+ )
922
and not is_prefill
923
and force_use_trtllm_attention() is not False
924
and cache_dtype.startswith("fp8")
@@ -868,8 +927,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
927
928
# Otherwise, match Q dtype to the KV cache dtype.
929
if cache_dtype.startswith("fp8"):
871
- # FP8-Q requires an fp8 tensor-core attention path
872
- # (FI native fa3 on SM90, trtllm-gen/XQA on SM100).
930
+ # FP8-Q requires an fp8 tensor-core attention path.
931
# Architectures with only fa2 (e.g. SM89, SM120) cannot
932
# consume FP8 queries, so keep the model dtype for Q there.
933
if current_platform.is_device_capability(
@@ -901,13 +959,17 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
959
) -> AttentionCGSupport:
960
"""Get the cudagraph support level for FlashInfer attention.
961
904
- The SM90 XQA integration only enables single-token decode today. Keep
905
- specdec CUDA graphs limited to trtllm-gen until vLLM wires the XQA
906
- specdec mask.
962
+ SM90 XQA supports only single-token decode. SM12x uses the dedicated
963
+ XQA API, which supports speculative and non-causal decode.
964
"""
965
if current_platform.is_device_capability(90):
966
return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
967
968
+ is_sm12x = current_platform.is_device_capability_family(120)
969
+ # XQA does not return LSE and therefore does not support DCP.
970
+ if is_sm12x and vllm_config.parallel_config.decode_context_parallel_size > 1:
971
+ return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
972
+
973
# For UniformTypeKVCacheSpecs, check all contained specs
974
kv_specs = (
975
kv_cache_spec.kv_cache_specs.values()
@@ -931,8 +993,9 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
993
has_trtllm_support = False
994
break
995
934
- # trtllm-gen only supports causal attention.
935
- if has_trtllm_support and not vllm_config.attention_config.use_non_causal:
996
+ if has_trtllm_support and (
997
+ is_sm12x or not vllm_config.attention_config.use_non_causal
998
+ ):
999
return AttentionCGSupport.UNIFORM_BATCH
1000
else:
1001
return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
@@ -967,11 +1030,68 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1030
1031
@staticmethod
1032
def _get_flashinfer_trtllm_api_decode_kernel() -> FlashInferDecodeKernel:
970
- if current_platform.is_device_capability(90):
1033
+ if current_platform.is_device_capability(
1034
+ 90
1035
+ ) or current_platform.is_device_capability_family(120):
1036
return FlashInferDecodeKernel.XQA
1037
assert current_platform.is_device_capability_family(100)
1038
return FlashInferDecodeKernel.TRTLLM_GEN
1039
1040
+ def _compute_decode_query_lens(
1041
+ self,
1042
+ qo_indptr: torch.Tensor,
1043
+ qo_indptr_cpu: torch.Tensor,
1044
+ num_decodes: int,
1045
+ num_decode_tokens: int,
1046
+ ) -> tuple[int, torch.Tensor | None, list[int] | None]:
1047
+ """Return the query width, ragged offsets, and effective query lengths."""
1048
+ assert self.use_dedicated_xqa
1049
+ if num_decodes == 0 or num_decode_tokens == 0:
1050
+ return 1, None, None
1051
+
1052
+ decode_q_lens = qo_indptr_cpu[1 : num_decodes + 1] - qo_indptr_cpu[:num_decodes]
1053
+ nonzero = decode_q_lens[decode_q_lens > 0]
1054
+ q_len_per_req = int(nonzero.max().item()) if nonzero.numel() > 0 else 1
1055
+ uniform = nonzero.numel() <= 1 or bool((nonzero == nonzero[0]).all().item())
1056
+ if uniform and num_decode_tokens == num_decodes * q_len_per_req:
1057
+ return q_len_per_req, None, None
1058
+
1059
+ if q_len_per_req <= 1:
1060
+ return 1, None, None
1061
+
1062
+ q_lens = decode_q_lens.tolist()
1063
+ return (
1064
+ q_len_per_req,
1065
+ qo_indptr[: num_decodes + 1],
1066
+ q_lens,
1067
+ )
1068
+
1069
+ def _get_decode_mask(
1070
+ self,
1071
+ q_len_per_req: int,
1072
+ ragged_q_lens: list[int] | None,
1073
+ num_decodes: int,
1074
+ causal: bool,
1075
+ ) -> torch.Tensor | None:
1076
+ """Return the packed XQA draft mask for speculative decode."""
1077
+ if q_len_per_req <= 1:
1078
+ return None
1079
+
1080
+ if ragged_q_lens is None:
1081
+ key = (q_len_per_req, causal)
1082
+ buf = self._decode_mask_cache.get(key)
1083
+ if buf is None:
1084
+ per_req = _make_xqa_draft_block_mask(q_len_per_req, causal, self.device)
1085
+ buf = (
1086
+ per_req.unsqueeze(0).expand(self.max_num_reqs, -1, -1).contiguous()
1087
+ )
1088
+ self._decode_mask_cache[key] = buf
1089
+ return buf[:num_decodes]
1090
+
1091
+ return _make_xqa_ragged_draft_block_mask(
1092
+ ragged_q_lens, q_len_per_req, causal, self.device
1093
+ )
1094
+
1095
def _get_prefill_wrapper(
1096
self,
1097
causal: bool = True,
@@ -1125,17 +1245,16 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1245
num_reqs = common_attn_metadata.num_reqs
1246
num_actual_tokens = common_attn_metadata.num_actual_tokens
1247
causal = common_attn_metadata.causal
1128
- if causal:
1248
+ route_decode = causal or self.use_dedicated_xqa
1249
+ if route_decode:
1250
num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
1251
split_decodes_and_prefills(
1252
common_attn_metadata,
1253
decode_threshold=self.reorder_batch_threshold,
1133
- require_uniform=True,
1254
+ require_uniform=not self.use_dedicated_xqa,
1255
)
1256
)
1257
else:
1137
- # FlashInfer decode/TRTLLM paths cannot express non-causal
1138
- # query-query attention, so DFlash runs as native prefill.
1258
num_decodes = 0
1259
num_prefills = num_reqs
1260
num_decode_tokens = 0
@@ -1171,7 +1290,9 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1290
has_sinks=self.has_sinks,
1291
has_spec=uses_spec_reorder,
1292
)
1174
- decode_with_flashinfer_trtllm_api = causal and self.use_trtllm_decode_attention
1293
+ decode_with_flashinfer_trtllm_api = self.use_trtllm_decode_attention and (
1294
+ causal or self.use_dedicated_xqa
1295
+ )
1296
1297
if not causal and self.use_dcp:
1298
raise NotImplementedError(
@@ -1188,7 +1309,9 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1309
)
1310
1311
if not all_uses_trtllm:
1191
- if self.has_sinks:
1312
+ if self.has_sinks and (
1313
+ not self.use_dedicated_xqa or self.use_dcp or use_cascade
1314
+ ):
1315
raise NotImplementedError(
1316
"FlashInfer backend currently does not support attention "
1317
"sinks, please use trtllm on blackwell or flash attention "
@@ -1455,22 +1578,44 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1578
## DECODE PATHWAY
1579
if num_decodes > 0:
1580
if decode_with_flashinfer_trtllm_api:
1458
- assert num_decode_tokens % num_decodes == 0, (
1459
- "XQA/trtllm-gen decode requires uniform query lengths per request. "
1460
- f"Got {num_decode_tokens=} and {num_decodes=}."
1461
- )
1581
assert self.flashinfer_trtllm_api_decode_kernel is not None
1582
+ if not self.use_dedicated_xqa:
1583
+ assert num_decode_tokens % num_decodes == 0, (
1584
+ "XQA/trtllm-gen decode requires uniform query lengths "
1585
+ f"per request. Got {num_decode_tokens=} and {num_decodes=}."
1586
+ )
1587
seq_lens_decode = seq_lens[:num_decodes]
1588
if self.use_dcp:
1589
assert common_attn_metadata.dcp_local_seq_lens is not None
1590
seq_lens_decode = common_attn_metadata.dcp_local_seq_lens[
1591
:num_decodes
1592
]
1593
+ q_len_per_req = 1
1594
+ q_cu_seq_lens = None
1595
+ decode_mask = None
1596
+ if self.use_dedicated_xqa:
1597
+ q_len_per_req, q_cu_seq_lens, ragged_q_lens = (
1598
+ self._compute_decode_query_lens(
1599
+ qo_indptr,
1600
+ qo_indptr_cpu,
1601
+ num_decodes,
1602
+ num_decode_tokens,
1603
+ )
1604
+ )
1605
+ decode_mask = self._get_decode_mask(
1606
+ q_len_per_req,
1607
+ ragged_q_lens,
1608
+ num_decodes,
1609
+ bool(causal),
1610
+ )
1611
attn_metadata.decode = FlashInferTrtllmAPIDecode(
1612
kernel=self.flashinfer_trtllm_api_decode_kernel,
1613
block_tables=block_table_tensor[:num_decodes],
1614
seq_lens=seq_lens_decode,
1615
max_seq_len=max_seq_len,
1616
+ q_len_per_req=q_len_per_req,
1617
+ q_cu_seq_lens=q_cu_seq_lens,
1618
+ mask=decode_mask,
1619
)
1620
else:
1621
assert seq_lens_cpu is not None
@@ -1856,6 +2001,14 @@ class FlashInferImpl(AttentionImpl):
2001
kv_cache_tuple = kv_cache_permute.split(hs, dim=-1)
2002
2003
use_dcp = self.dcp_world_size > 1
2004
+ decode_with_dedicated_xqa = (
2005
+ decode_with_xqa and current_platform.is_device_capability_family(120)
2006
+ )
2007
+ if decode_with_dedicated_xqa:
2008
+ assert not use_dcp
2009
+ assert not self.is_kvcache_nvfp4
2010
+ assert self.o_sf_scale is None
2011
+ assert output.dtype != FP4_DTYPE
2012
2013
# Regular attention (common case).
2014
# Decodes are at the front and prefills are at the back.
@@ -1935,6 +2088,7 @@ class FlashInferImpl(AttentionImpl):
2088
v_scale=layer._v_scale_float,
2089
out=out_prefill,
2090
kv_cache_sf=kv_cache_sf,
2091
+ sinks=self.sinks,
2092
)
2093
2094
if needs_fp8_out_prefill:
@@ -2103,6 +2257,7 @@ class FlashInferImpl(AttentionImpl):
2257
lse=lse,
2258
return_lse=True,
2259
kv_cache_sf=kv_cache_sf,
2260
+ sinks=self.sinks,
2261
)
2262
output[:num_decode_tokens] = self.dcp_combine(
2263
output_tmp,
@@ -2118,6 +2273,7 @@ class FlashInferImpl(AttentionImpl):
2273
v_scale=layer._v_scale_float,
2274
out=out_decode,
2275
kv_cache_sf=kv_cache_sf,
2276
+ sinks=self.sinks,
2277
)
2278
2279
if needs_fp8_out:
@@ -2165,6 +2321,31 @@ class FlashInferImpl(AttentionImpl):
2321
)
2322
decode_query = canonicalize_singleton_dim_strides(decode_query)
2323
2324
+ if decode_with_dedicated_xqa:
2325
+ bmm1_scale = self.get_xqa_bmm1_scale(
2326
+ layer, attn_metadata.q_data_type_decode
2327
+ )
2328
+ q_len_per_req = attn_metadata.decode.q_len_per_req
2329
+
2330
+ flashinfer_xqa_batch_decode_with_kv_cache(
2331
+ query=decode_query,
2332
+ kv_cache=kv_cache_tuple,
2333
+ workspace_buffer=workspace_buffer,
2334
+ block_tables=block_tables_decode,
2335
+ seq_lens=seq_lens_decode,
2336
+ max_seq_len=attn_metadata.decode.max_seq_len,
2337
+ bmm1_scale=bmm1_scale,
2338
+ bmm2_scale=self.bmm2_scale,
2339
+ window_left=self.window_left,
2340
+ out=output[:num_decode_tokens],
2341
+ sinks=self.sinks,
2342
+ kv_layout=get_kv_cache_layout(),
2343
+ q_len_per_req=q_len_per_req,
2344
+ mask=attn_metadata.decode.mask,
2345
+ q_cu_seq_lens=attn_metadata.decode.q_cu_seq_lens,
2346
+ )
2347
+ return output_padded
2348
+
2349
if output.dtype == FP4_DTYPE:
2350
assert self.o_sf_scale is not None
2351
out = FP4Tensor(