Xqa decode kernels (#43232)
Signed-off-by: Dan Blanaru <48605845+DanBlanaru@users.noreply.github.com> Co-authored-by: Matthew Bonanni <mbonanni@redhat.com>
Dan Blanaru committed
Jul 2, 2026 at 21:32 UTC
d29125c0852eb61efef060d55675010d1abf51fe
7 files changed
+538
-143
docs/design/attention_backends.md
+3
-2
@@ -161,7 +161,8 @@ Priority is **1 = highest** (tried first).
161
| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | --------- | --- | --------------- | ------------ |
162
| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ✅ | ❌ | ❌ | All | N/A |
163
| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ❌ | ✅ | ❌ | ✅ | Decoder | 8.x-9.x |
164
-| `FLASHINFER` | TRTLLM† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ✅ | ✅ | ❌ | ✅ | Decoder | 10.x |
164
+| `FLASHINFER` | XQA† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ❌ | ❌ | ❌ | ✅ | Decoder | 9.0 |
165
+| `FLASHINFER` | trtllm-gen† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ✅ | ✅ | ❌ | ✅ | Decoder | 10.x |
166
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | ✅ | All | ≥8.0 |
167
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
168
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
@@ -175,7 +176,7 @@ Priority is **1 = highest** (tried first).
176
| `TRITON_ATTN_DIFFKV` | | fp16, bf16 | `auto`, `bfloat16` | Any | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
177
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
178
178
-> **†** FlashInfer uses TRTLLM attention on Blackwell (SM100), which supports sinks. Disable via `--attention-config.use_trtllm_attention=0`.
179
+> **†** FlashInfer Native is the regular FlashInfer path. XQA is the SM90 decode path exposed through FlashInfer's TRTLLM decode API. trtllm-gen is used on SM100 and supports sinks. Disable XQA/trtllm-gen via `--attention-config.use_trtllm_attention=0`.
180
>
181
> **\*** Specify the FlashAttention version via `--attention-config.flash_attn_version=2`, `3`, or `4`. Default is FA4 on SM100+ (Blackwell), FA3 on SM90 (Hopper), FA2 otherwise.
182
tests/kernels/attention/test_use_trtllm_attention.py
+16
-1
@@ -78,14 +78,29 @@ def test_supports_sm100_with_artifactory(_art, _cap):
78
79
80
@patch("vllm.envs.VLLM_BATCH_INVARIANT", False)
81
+@patch(
82
+ "vllm.utils.flashinfer.current_platform.is_device_capability", return_value=False
83
+)
84
@patch(
85
"vllm.utils.flashinfer.current_platform.is_device_capability_family",
86
return_value=False,
87
)
85
-def test_supports_non_sm100_platform(_cap):
88
+def test_supports_unsupported_platform(_family, _cap):
89
assert supports_trtllm_attention() is False
90
91
92
+@patch("vllm.envs.VLLM_BATCH_INVARIANT", False)
93
+@patch("vllm.utils.flashinfer.current_platform.is_device_capability", return_value=True)
94
+@patch(
95
+ "vllm.utils.flashinfer.current_platform.is_device_capability_family",
96
+ return_value=False,
97
+)
98
+@patch("vllm.utils.flashinfer.has_nvidia_artifactory", return_value=True)
99
+def test_supports_sm90_decode_only(_art, _family, _cap):
100
+ assert supports_trtllm_attention(is_prefill=False) is True
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_family",
tests/v1/attention/test_attention_backends.py
+120
-4
@@ -16,7 +16,7 @@ from tests.v1.attention.utils import (
16
try_backend_includes_kv_cache_update,
17
try_get_attention_backend,
18
)
19
-from vllm.config import ModelConfig
19
+from vllm.config import ModelConfig, set_current_vllm_config
20
from vllm.platforms import current_platform
21
from vllm.utils.math_utils import cdiv
22
from vllm.utils.torch_utils import (
@@ -24,7 +24,11 @@ from vllm.utils.torch_utils import (
24
is_torch_equal_or_newer,
25
set_random_seed,
26
)
27
-from vllm.v1.attention.backend import AttentionType, CommonAttentionMetadata
27
+from vllm.v1.attention.backend import (
28
+ AttentionCGSupport,
29
+ AttentionType,
30
+ CommonAttentionMetadata,
31
+)
32
from vllm.v1.attention.backends.registry import AttentionBackendEnum
33
from vllm.v1.attention.backends.utils import (
34
set_kv_cache_layout,
@@ -626,6 +630,118 @@ def test_causal_backend_correctness(
630
)
631
632
633
+@pytest.mark.skipif(
634
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
635
+ reason="FlashInfer is not available.",
636
+)
637
+def test_flashinfer_xqa_bmm1_scale_matches_decode_q_dtype():
638
+ """XQA decode should only apply q_scale when decode Q is FP8."""
639
+ from vllm.v1.attention.backends import flashinfer as flashinfer_backend
640
+
641
+ class MockLayer:
642
+ _q_scale_float = 2.0
643
+ _k_scale_float = 3.0
644
+
645
+ impl = object.__new__(flashinfer_backend.FlashInferImpl)
646
+ impl.scale = 0.5
647
+ impl.kv_cache_dtype = "fp8"
648
+
649
+ assert impl.get_xqa_bmm1_scale(MockLayer, torch.bfloat16) == 1.5
650
+ assert impl.get_xqa_bmm1_scale(MockLayer, torch.float8_e4m3fn) == 3.0
651
+
652
+
653
+@pytest.mark.skipif(
654
+ AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
655
+ reason="FlashInfer is not available.",
656
+)
657
+def test_flashinfer_sm90_xqa_decode_correctness(default_vllm_config):
658
+ """FlashInfer should route Hopper decode through XQA and match SDPA."""
659
+ if not current_platform.is_cuda() or not current_platform.is_device_capability(90):
660
+ pytest.skip("FlashInfer XQA decode requires SM90.")
661
+
662
+ import unittest.mock
663
+
664
+ from vllm.utils.flashinfer import can_use_trtllm_attention
665
+ from vllm.v1.attention.backends import flashinfer as flashinfer_backend
666
+ from vllm.v1.attention.backends.utils import PerLayerParameters
667
+
668
+ def mock_get_per_layer_parameters(vllm_config, layer_names, impl_cls):
669
+ return {
670
+ "placeholder": PerLayerParameters(
671
+ window_left=-1,
672
+ logits_soft_cap=0.0,
673
+ sm_scale=1.0,
674
+ )
675
+ }
676
+
677
+ def causal_mask_mod(
678
+ b: torch.Tensor,
679
+ h: torch.Tensor,
680
+ q_idx: torch.Tensor,
681
+ kv_idx: torch.Tensor,
682
+ *,
683
+ context_len: int,
684
+ ):
685
+ return (q_idx + context_len) >= kv_idx
686
+
687
+ batch_spec = BATCH_SPECS["small_decode"]
688
+ vllm_config = create_vllm_config(
689
+ model_name="meta-llama/Meta-Llama-3-8B",
690
+ max_model_len=max(batch_spec.seq_lens),
691
+ block_size=16,
692
+ )
693
+ device = torch.device(f"{DEVICE_TYPE}:0")
694
+ kv_cache_spec = FullAttentionSpec(
695
+ block_size=vllm_config.cache_config.block_size,
696
+ num_kv_heads=vllm_config.model_config.get_num_kv_heads(
697
+ vllm_config.parallel_config
698
+ ),
699
+ head_size=vllm_config.model_config.get_head_size(),
700
+ dtype=vllm_config.model_config.dtype,
701
+ )
702
+
703
+ with set_current_vllm_config(vllm_config):
704
+ if not can_use_trtllm_attention(
705
+ vllm_config.model_config.get_num_attention_heads(
706
+ vllm_config.parallel_config
707
+ ),
708
+ kv_cache_spec.num_kv_heads,
709
+ is_prefill=False,
710
+ ):
711
+ pytest.skip("FlashInfer XQA decode is not available in this setup.")
712
+
713
+ with unittest.mock.patch(
714
+ "vllm.v1.attention.backends.flashinfer.get_per_layer_parameters",
715
+ mock_get_per_layer_parameters,
716
+ ):
717
+ builder = flashinfer_backend.FlashInferMetadataBuilder(
718
+ kv_cache_spec, ["placeholder"], vllm_config, device
719
+ )
720
+ common_attn_metadata = create_common_attn_metadata(
721
+ batch_spec, vllm_config.cache_config.block_size, device
722
+ )
723
+ attn_metadata = builder.build(0, common_attn_metadata)
724
+
725
+ assert (
726
+ flashinfer_backend.FlashInferMetadataBuilder.get_cudagraph_support(
727
+ vllm_config, kv_cache_spec
728
+ )
729
+ == AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
730
+ )
731
+ assert isinstance(
732
+ attn_metadata.decode,
733
+ flashinfer_backend.FlashInferTrtllmAPIDecode,
734
+ )
735
+ assert attn_metadata.decode.kernel == flashinfer_backend.FlashInferDecodeKernel.XQA
736
+
737
+ _test_backend_correctness(
738
+ batch_spec,
739
+ "meta-llama/Meta-Llama-3-8B",
740
+ [AttentionBackendEnum.FLASHINFER],
741
+ causal_mask_mod,
742
+ )
743
+
744
+
745
if current_platform.is_rocm():
746
# FLASH_ATTN is not supported on ROCm
747
SLIDING_WINDOW_BACKENDS_TO_TEST = [
@@ -656,7 +772,7 @@ else:
772
@pytest.mark.parametrize("model", ["microsoft/Phi-tiny-MoE-instruct"])
773
@pytest.mark.parametrize("tensor_parallel_size", [1, 2, 4])
774
def test_sliding_window_backend_correctness(
659
- batch_spec_name: str, model: str, tensor_parallel_size: int
775
+ default_vllm_config, batch_spec_name: str, model: str, tensor_parallel_size: int
776
):
777
"""Test backend's correctness with sliding window attention."""
778
@@ -718,7 +834,7 @@ def test_sliding_window_backend_correctness(
834
@pytest.mark.parametrize("model", ["google/embeddinggemma-300m"])
835
@pytest.mark.parametrize("tensor_parallel_size", [1, 2])
836
def test_sliding_window_encoder_backend_correctness(
721
- batch_spec_name: str, model: str, tensor_parallel_size: int
837
+ default_vllm_config, batch_spec_name: str, model: str, tensor_parallel_size: int
838
):
839
"""Test backend's correctness with sliding window attention."""
840
tests/v1/attention/test_trtllm_attention_integration.py
+6
-3
@@ -32,9 +32,10 @@ if not current_platform.is_device_capability_family(100):
32
)
33
34
from vllm.v1.attention.backends.flashinfer import ( # noqa: E402
35
+ FlashInferDecodeKernel,
36
FlashInferImpl,
37
FlashInferMetadataBuilder,
37
- TRTLLMDecode,
38
+ FlashInferTrtllmAPIDecode,
39
TRTLLMPrefill,
40
)
41
@@ -435,9 +436,11 @@ def _run_trtllm_integration(batch_spec, kv_cache_dtype="auto", model_name=MODEL)
436
f"Expected TRTLLMPrefill, got {type(attn_metadata.prefill)}"
437
)
438
if has_decodes:
438
- assert isinstance(attn_metadata.decode, TRTLLMDecode), (
439
- f"Expected TRTLLMDecode, got {type(attn_metadata.decode)}"
439
+ assert isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode), (
440
+ "Expected FlashInferTrtllmAPIDecode, got "
441
+ f"{type(attn_metadata.decode)}"
442
)
443
+ assert attn_metadata.decode.kernel == FlashInferDecodeKernel.TRTLLM_GEN
444
445
impl = FlashInferImpl(
446
num_heads=num_q_heads,
tools/pre_commit/generate_attention_backend_docs.py
+70
-23
@@ -261,6 +261,25 @@ def _find_cc_in_function(tree: ast.AST, func_name: str) -> str | None:
261
return None
262
263
264
+def _find_exact_cc_in_function(tree: ast.AST, func_name: str) -> str | None:
265
+ """Find a compute capability from is_device_capability() calls in a function."""
266
+ for node in ast.walk(tree):
267
+ if not isinstance(node, ast.FunctionDef) or node.name != func_name:
268
+ continue
269
+ for n in ast.walk(node):
270
+ if (
271
+ isinstance(n, ast.Call)
272
+ and isinstance(n.func, ast.Attribute)
273
+ and n.func.attr == "is_device_capability"
274
+ and n.args
275
+ and isinstance(n.args[0], ast.Constant)
276
+ and isinstance(n.args[0].value, int)
277
+ ):
278
+ capability = n.args[0].value
279
+ return f"{capability // 10}.{capability % 10}"
280
+ return None
281
+
282
+
283
# ---------------------------------------------------------------------------
284
# Registry and file resolution
285
# ---------------------------------------------------------------------------
@@ -1048,10 +1067,11 @@ def parse_flash_attn_features() -> dict[str, dict[str, Any]]:
1067
1068
1069
def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
1051
- """Parse flashinfer.py to detect TRTLLM-specific features.
1070
+ """Parse flashinfer.py to detect FlashInfer TRTLLM API variants.
1071
1053
- FLASHINFER uses TRTLLM attention on SM100 (Blackwell), which has different
1054
- capabilities (e.g., sink support) than native FlashInfer on earlier GPUs.
1072
+ FLASHINFER uses XQA on SM90 and trtllm-gen on SM100 through FlashInfer's
1073
+ TRTLLM decode API. These variants have different capabilities than native
1074
+ FlashInfer.
1075
"""
1076
if not FLASHINFER_UTILS_FILE.exists():
1077
return {}
@@ -1061,9 +1081,10 @@ def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
1081
except Exception:
1082
return {}
1083
1064
- trtllm_compute_cap = _find_cc_in_function(tree, "supports_trtllm_attention")
1084
+ xqa_compute_cap = _find_exact_cc_in_function(tree, "supports_trtllm_attention")
1085
+ trtllm_gen_compute_cap = _find_cc_in_function(tree, "supports_trtllm_attention")
1086
1066
- if not trtllm_compute_cap:
1087
+ if not xqa_compute_cap and not trtllm_gen_compute_cap:
1088
return {}
1089
1090
# KV cache dtypes that only work with a dedicated kernel (e.g. nvfp4
@@ -1073,12 +1094,17 @@ def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
1094
1095
return {
1096
"native": {
1076
- # Native FlashInfer: everything except SM100
1097
+ # Native FlashInfer path.
1098
"supports_sink": False,
1099
},
1079
- "trtllm": {
1080
- # TRTLLM pathway on Blackwell
1081
- "compute_capability": trtllm_compute_cap,
1100
+ "xqa": {
1101
+ # XQA decode path on Hopper.
1102
+ "compute_capability": xqa_compute_cap,
1103
+ "supports_sink": False,
1104
+ },
1105
+ "trtllm_gen": {
1106
+ # trtllm-gen pathway on Blackwell.
1107
+ "compute_capability": trtllm_gen_compute_cap,
1108
"supports_sink": True,
1109
},
1110
"exclude_kv_dtypes": kernel_only_kv_dtypes,
@@ -1086,7 +1112,7 @@ def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
1112
1113
1114
# ---------------------------------------------------------------------------
1089
-# Backend variant expansion (FA2/FA3/FA4, FlashInfer native/TRTLLM)
1115
+# Backend variant expansion (FA2/FA3/FA4, FlashInfer native/XQA/trtllm-gen)
1116
# ---------------------------------------------------------------------------
1117
1118
@@ -1146,7 +1172,7 @@ def _expand_flashinfer_variants(
1172
all_backends: list[dict[str, Any]],
1173
fi_features: dict[str, dict[str, Any]],
1174
) -> list[dict[str, Any]]:
1149
- """Expand FLASHINFER into native and TRTLLM variants."""
1175
+ """Expand FLASHINFER into native, XQA, and trtllm-gen variants."""
1176
expanded = []
1177
for backend in all_backends:
1178
if backend["name"] != "FLASHINFER":
@@ -1157,9 +1183,8 @@ def _expand_flashinfer_variants(
1183
orig_cap = backend["compute_capability"]
1184
parts = orig_cap.replace(".x", "").split("-")
1185
min_cc = parts[0] if parts else "7"
1160
- trtllm_cc = fi_features["trtllm"]["compute_capability"]
1186
1162
- # Create native entry (pre-Blackwell GPUs)
1187
+ # Create native entry.
1188
native = backend.copy()
1189
native["version"] = "Native†"
1190
native["_sort_key"] = "FLASHINFER"
@@ -1176,16 +1201,36 @@ def _expand_flashinfer_variants(
1201
if d not in exclude
1202
)
1203
1179
- # Create TRTLLM entry
1180
- trtllm = backend.copy()
1181
- trtllm["version"] = "TRTLLM†"
1182
- trtllm["_sort_key"] = "FLASHINFER"
1183
- trtllm["_sort_order"] = 1
1184
- trtllm["compute_capability"] = trtllm_cc
1185
- trtllm["supports_sink"] = fi_features["trtllm"]["supports_sink"]
1204
+ # Create XQA entry.
1205
+ xqa = backend.copy()
1206
+ xqa["version"] = "XQA†"
1207
+ xqa["_sort_key"] = "FLASHINFER"
1208
+ xqa["_sort_order"] = 1
1209
+ xqa["compute_capability"] = fi_features["xqa"]["compute_capability"]
1210
+ xqa["supports_sink"] = fi_features["xqa"]["supports_sink"]
1211
+ xqa["supports_non_causal"] = False
1212
+ if exclude:
1213
+ xqa["kv_cache_dtypes"] = ", ".join(
1214
+ d
1215
+ for d in (d.strip() for d in xqa["kv_cache_dtypes"].split(","))
1216
+ if d not in exclude
1217
+ )
1218
+
1219
+ # Create trtllm-gen entry.
1220
+ trtllm_gen = backend.copy()
1221
+ trtllm_gen["version"] = "trtllm-gen†"
1222
+ trtllm_gen["_sort_key"] = "FLASHINFER"
1223
+ trtllm_gen["_sort_order"] = 2
1224
+ trtllm_gen["compute_capability"] = fi_features["trtllm_gen"][
1225
+ "compute_capability"
1226
+ ]
1227
+ trtllm_gen["supports_sink"] = fi_features["trtllm_gen"]["supports_sink"]
1228
1229
expanded.append(native)
1188
- expanded.append(trtllm)
1230
+ if fi_features["xqa"]["compute_capability"]:
1231
+ expanded.append(xqa)
1232
+ if fi_features["trtllm_gen"]["compute_capability"]:
1233
+ expanded.append(trtllm_gen)
1234
return expanded
1235
1236
@@ -1788,8 +1833,10 @@ def generate_docs() -> str:
1833
footnotes = []
1834
if fi_features:
1835
footnotes.append(
1791
- "> **†** FlashInfer uses TRTLLM attention on Blackwell (SM100), which "
1792
- "supports sinks. Disable via `--attention-config.use_trtllm_attention=0`."
1836
+ "> **†** FlashInfer Native is the regular FlashInfer path. XQA is the "
1837
+ "SM90 decode path exposed through FlashInfer's TRTLLM decode API. "
1838
+ "trtllm-gen is used on SM100 and supports sinks. Disable XQA/trtllm-gen "
1839
+ "via `--attention-config.use_trtllm_attention=0`."
1840
)
1841
if fa_features:
1842
footnotes.append(
vllm/utils/flashinfer.py
+37
-18
@@ -371,19 +371,27 @@ def has_nvidia_artifactory() -> bool:
371
372
373
@functools.cache
374
-def supports_trtllm_attention() -> bool:
375
- """
376
- TRTLLM attention is supported if the platform is SM100,
377
- NVIDIA artifactory is accessible, and batch-invariant mode is not enabled.
374
+def supports_trtllm_attention(is_prefill: bool = False) -> bool:
375
+ """Return whether TRTLLM attention is available on the current platform
376
+ for the given attention phase.
377
+
378
+ SM90 (Hopper) supports the XQA decode kernel but not TRTLLM prefill.
379
+ SM100+ supports TRTLLM for both phases. All others are unsupported.
380
"""
381
# Batch-invariant mode disables TRTLLM attention
382
if envs.VLLM_BATCH_INVARIANT:
383
return False
384
383
- # Requires SM100 and NVIDIA artifactory to be accessible to download cubins
384
- return (
385
- current_platform.is_device_capability_family(100) and has_nvidia_artifactory()
386
- )
385
+ # Requires NVIDIA artifactory to be accessible to download cubins
386
+ if not has_nvidia_artifactory():
387
+ return False
388
+
389
+ # SM90 has XQA decode; prefill is not supported.
390
+ if current_platform.is_device_capability(90):
391
+ return not is_prefill
392
+
393
+ # SM100/SM103 has both prefill and decode TRTLLM kernels.
394
+ return current_platform.is_device_capability_family(100)
395
396
397
def force_use_trtllm_attention() -> bool | None:
@@ -400,12 +408,15 @@ def force_use_trtllm_attention() -> bool | None:
408
return vllm_config.attention_config.use_trtllm_attention
409
410
403
-def can_use_trtllm_attention(num_qo_heads: int, num_kv_heads: int) -> bool:
411
+def can_use_trtllm_attention(
412
+ num_qo_heads: int, num_kv_heads: int, is_prefill: bool = False
413
+) -> bool:
414
"""Check if the current configuration supports TRTLLM attention."""
415
if force_use_trtllm_attention() is False:
416
return False
407
- has_trtllm = supports_trtllm_attention()
408
- return has_trtllm and (num_qo_heads % num_kv_heads == 0)
417
+ return supports_trtllm_attention(is_prefill=is_prefill) and (
418
+ num_qo_heads % num_kv_heads == 0
419
+ )
420
421
422
def use_trtllm_attention(
@@ -437,11 +448,12 @@ def use_trtllm_attention(
448
return False
449
450
# The platform is not supported
440
- if not supports_trtllm_attention():
451
+ if not supports_trtllm_attention(is_prefill=is_prefill):
452
if force_use_trtllm:
453
logger.warning_once(
443
- "TRTLLM attention is not supported on this platform, "
444
- "but --attention-config.use_trtllm_attention is set to 1"
454
+ "TRTLLM attention is not supported on this platform for %s, "
455
+ "but --attention-config.use_trtllm_attention is set to 1",
456
+ "prefill" if is_prefill else "decode",
457
)
458
return False
459
@@ -476,13 +488,20 @@ def use_trtllm_attention(
488
if is_prefill:
489
# Prefill auto-detection
490
use_trtllm = kv_cache_dtype == "auto"
479
- if use_trtllm:
480
- logger.warning_once("Using TRTLLM prefill attention (auto-detected).")
491
+ elif current_platform.is_device_capability(90) and kv_cache_dtype.startswith(
492
+ "fp8"
493
+ ):
494
+ # SM90 + FP8 KV cache: prefer the XQA decode kernel. XQA does not
495
+ # support NVFP4 KV (that is an SM100 trtllm-gen path only).
496
+ use_trtllm = True
497
else:
498
# Decode auto-detection
499
use_trtllm = num_tokens <= 256 and kv_cache_dtype == "auto"
484
- if use_trtllm:
485
- logger.warning_once("Using TRTLLM decode attention (auto-detected).")
500
+ if use_trtllm:
501
+ logger.warning_once(
502
+ "Using TRTLLM %s attention (auto-detected).",
503
+ "prefill" if is_prefill else "decode",
504
+ )
505
return use_trtllm
506
507
# CLI argument is set to 1 - respect it
vllm/v1/attention/backends/flashinfer.py
+286
-92
@@ -3,6 +3,7 @@
3
"""Attention layer with FlashInfer."""
4
5
from dataclasses import dataclass
6
+from enum import Enum
7
from functools import partial
8
from typing import ClassVar
9
@@ -19,6 +20,7 @@ from flashinfer.prefill import trtllm_batch_context_with_kv_cache
20
from flashinfer.utils import FP4Tensor
21
from typing_extensions import override
22
23
+from vllm import _custom_ops as custom_ops
24
from vllm import envs
25
from vllm.config import (
26
CUDAGraphMode,
@@ -38,6 +40,8 @@ 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
+ force_use_trtllm_attention,
44
+ supports_trtllm_attention,
45
use_trtllm_attention,
46
)
47
from vllm.utils.math_utils import cdiv
@@ -84,16 +88,16 @@ FP4_DTYPE = torch.uint8
88
89
logger = init_logger(__name__)
90
87
-trtllm_gen_workspace_buffer = None
91
+trtllm_workspace_buffer = None
92
93
90
-def _get_trtllm_gen_workspace_buffer():
91
- global trtllm_gen_workspace_buffer
92
- if trtllm_gen_workspace_buffer is None:
93
- trtllm_gen_workspace_buffer = torch.zeros(
94
+def _get_trtllm_workspace_buffer():
95
+ global trtllm_workspace_buffer
96
+ if trtllm_workspace_buffer is None:
97
+ trtllm_workspace_buffer = torch.zeros(
98
envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE, dtype=torch.uint8, device="cuda"
99
)
96
- return trtllm_gen_workspace_buffer
100
+ return trtllm_workspace_buffer
101
102
103
@triton.jit
@@ -350,6 +354,7 @@ class FlashInferBackend(AttentionBackend):
354
use_large_pages = (
355
num_kv_heads > 0
356
and num_qo_heads // num_kv_heads > 1
357
+ and current_platform.is_device_capability_family(100)
358
and can_use_trtllm_attention(num_qo_heads, num_kv_heads)
359
)
360
if not use_large_pages:
@@ -418,6 +423,16 @@ class FlashInferBackend(AttentionBackend):
423
else:
424
raise ValueError(f"Unrecognized dtype: {kv_cache_dtype}")
425
426
+ @classmethod
427
+ def supports_kv_cache_dtype(cls, kv_cache_dtype: CacheDType | None) -> bool:
428
+ if kv_cache_dtype == "nvfp4":
429
+ return (
430
+ current_platform.is_device_capability_family(100)
431
+ and supports_trtllm_attention(is_prefill=True)
432
+ and supports_trtllm_attention(is_prefill=False)
433
+ )
434
+ return super().supports_kv_cache_dtype(kv_cache_dtype)
435
+
436
@classmethod
437
def get_supported_head_sizes(cls) -> list[int]:
438
# https://github.com/flashinfer-ai/flashinfer/blob/3d55c71a62052c590c130897d3a3db49b14fcc34/include/flashinfer/utils.cuh#L157
@@ -436,10 +451,9 @@ class FlashInferBackend(AttentionBackend):
451
452
@classmethod
453
def supports_sink(cls) -> bool:
439
- """FlashInfer supports sinks when TRTLLM attention is available (SM100)."""
454
+ """FlashInfer supports sinks only on the SM100 trtllm-gen path."""
455
from vllm.utils.flashinfer import (
456
force_use_trtllm_attention,
442
- supports_trtllm_attention,
457
)
458
459
# Respect explicit disable flag (e.g.,
@@ -447,8 +461,13 @@ class FlashInferBackend(AttentionBackend):
461
if force_use_trtllm_attention() is False:
462
return False
463
464
+ if not current_platform.is_device_capability_family(100):
465
+ return False
466
+
467
# Check if TRTLLM is supported on this platform
451
- return supports_trtllm_attention()
468
+ return supports_trtllm_attention(
469
+ is_prefill=False
470
+ ) and supports_trtllm_attention(is_prefill=True)
471
472
@classmethod
473
def get_required_kv_cache_layout(cls) -> KVCacheLayoutType | None:
@@ -474,6 +493,13 @@ class FIDecode:
493
wrapper: BatchDecodeWithPagedKVCacheWrapper
494
495
496
+class FlashInferDecodeKernel(Enum):
497
+ """Decode kernels selected inside the FlashInfer backend."""
498
+
499
+ XQA = "xqa"
500
+ TRTLLM_GEN = "trtllm-gen"
501
+
502
+
503
@dataclass
504
class TRTLLMPrefill:
505
"""Metadata for the TRTLLM prefill pathway."""
@@ -503,8 +529,15 @@ class TRTLLMPrefill:
529
530
531
@dataclass
506
-class TRTLLMDecode:
507
- """Metadata for the TRTLLM decode pathway."""
532
+class FlashInferTrtllmAPIDecode:
533
+ """Metadata for decode paths using FlashInfer's TRTLLM decode API.
534
+
535
+ FlashInfer exposes both XQA (SM90) and trtllm-gen (SM100) through
536
+ ``trtllm_batch_decode_with_kv_cache``. Keep them as distinct vLLM
537
+ decode kernels because their dtype/layout/output constraints differ.
538
+ """
539
+
540
+ kernel: FlashInferDecodeKernel
541
542
block_tables: torch.Tensor
543
"""
@@ -530,7 +563,10 @@ class FlashInferMetadata:
563
slot_mapping: torch.Tensor
564
"""Tensor for writing K/V to the cache. Shape: [num_actual_tokens]"""
565
533
- q_data_type: torch.dtype
566
+ # The data types of the query for prefill and decode.
567
+ # On SM90, these two data types may be different.
568
+ q_data_type_prefill: torch.dtype
569
+ q_data_type_decode: torch.dtype
570
571
num_decodes: int
572
num_decode_tokens: int
@@ -544,7 +580,7 @@ class FlashInferMetadata:
580
Will be `None` if `num_prefill_tokens == 0`.
581
"""
582
547
- decode: FIDecode | TRTLLMDecode | None
583
+ decode: FIDecode | FlashInferTrtllmAPIDecode | None
584
"""
585
Holds the metadata for the decode portion of the batch.
586
Will be `None` if `num_decode_tokens == 0`.
@@ -651,12 +687,14 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
687
# storage dtype may not be the same as the op dtype (uint8 vs fp8_e4m3)
688
self.is_kvcache_nvfp4 = self.cache_dtype == "nvfp4"
689
if self.is_kvcache_nvfp4:
654
- # trtllm-gen FP4 FMHA kernels only exist for sm100f (sm_100/sm_103).
655
- # Fail fast at init rather than crashing on the first request.
656
- if not current_platform.is_device_capability_family(100):
690
+ if (
691
+ force_use_trtllm_attention() is False
692
+ or not supports_trtllm_attention(is_prefill=True)
693
+ or not supports_trtllm_attention(is_prefill=False)
694
+ ):
695
raise ValueError(
658
- "--kv-cache-dtype nvfp4 requires sm100f, "
659
- "please try a different dtype or remove"
696
+ "--kv-cache-dtype nvfp4 requires the SM100 trtllm-gen "
697
+ "FlashInfer path."
698
)
699
# For NVFP4, kv_cache_dtype stays as the string "nvfp4"
700
# which is passed to FlashInferImpl
@@ -671,54 +709,87 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
709
assert self.kv_cache_spec.dtype == self.model_config.dtype
710
self.kv_cache_dtype = self.kv_cache_spec.dtype
711
674
- # Use model dtype as q dtype when TRTLLM attn is not supported, or
675
- # --attention-config.disable_flashinfer_q_quantization is set to 1. Otherwise,
676
- # try to use fp8 q if kv cache is fp8, and will fall back to model dtype
677
- # if TRTLLM attention kernel is not used when building attn metadata
678
- can_use_trtllm = can_use_trtllm_attention(self.num_qo_heads, self.num_kv_heads)
712
+ # Compute per-phase Q dtype. On SM90 (XQA decode), the prefill and
713
+ # decode phases require different Q dtypes when the KV cache is FP8
714
+ # (FP8-Q for the FI native prefill, BF16/FP16-Q for XQA decode),
715
+ # so both values must be tracked independently.
716
+ self.q_data_type_prefill = self.get_q_data_type(
717
+ vllm_config, self.kv_cache_spec, is_prefill=True
718
+ )
719
+ self.q_data_type_decode = self.get_q_data_type(
720
+ vllm_config, self.kv_cache_spec, is_prefill=False
721
+ )
722
723
+ # Prefer TRTLLM/XQA for decoding whenever supported. The decode kernel
724
+ # must be selected statically for FULL cudagraph capture.
725
+ can_use_xqa_or_trtllm_gen_decode = can_use_trtllm_attention(
726
+ self.num_qo_heads, self.num_kv_heads, is_prefill=False
727
+ )
728
# Page sizes >= 128 require the trtllm-gen GQA/MQA path (guaranteed by
729
# get_supported_kernel_block_sizes).
730
assert self.page_size <= 64 or (
683
- can_use_trtllm and self.num_qo_heads // self.num_kv_heads > 1
731
+ current_platform.is_device_capability_family(100)
732
+ and can_use_xqa_or_trtllm_gen_decode
733
+ and self.num_qo_heads // self.num_kv_heads > 1
734
), f"Unexpected FlashInfer page size {self.page_size} without trtllm-gen GQA"
685
-
686
- if (
687
- can_use_trtllm
688
- and not vllm_config.attention_config.disable_flashinfer_q_quantization
689
- ):
690
- if self.is_kvcache_nvfp4:
691
- # NVFP4 KV cache uses FP8 quantized queries
692
- self.q_data_type = FlashInferBackend.get_dtype_for_flashinfer(
693
- "fp8_e4m3"
694
- )
695
- else:
696
- self.q_data_type = self.kv_cache_dtype
697
- else:
698
- self.q_data_type = self.model_config.dtype
699
-
700
- # Prefer TRTLLM attention for decoding in all cases.
701
- # This allows us to use AttentionCGSupport.UNIFORM_BATCH mode.
702
- self.use_trtllm_decode_attention = can_use_trtllm
703
- self._init_reorder_batch_threshold(1, supports_spec_as_decode=can_use_trtllm)
735
+ self.use_trtllm_decode_attention = can_use_xqa_or_trtllm_gen_decode
736
+ self.flashinfer_trtllm_api_decode_kernel: FlashInferDecodeKernel | None = (
737
+ self._get_flashinfer_trtllm_api_decode_kernel()
738
+ if can_use_xqa_or_trtllm_gen_decode
739
+ else None
740
+ )
741
+ supports_spec_as_decode = (
742
+ self.flashinfer_trtllm_api_decode_kernel
743
+ == FlashInferDecodeKernel.TRTLLM_GEN
744
+ )
745
+ self._init_reorder_batch_threshold(
746
+ 1, supports_spec_as_decode=supports_spec_as_decode
747
+ )
748
749
self._cascade_wrapper = None # Wrapper for cascade attention
750
751
# Global hyperparameters shared by all attention layers
752
# TODO: discard this for trtllm-gen backend
709
- self.global_hyperparameters = infer_global_hyperparameters(
710
- get_per_layer_parameters(vllm_config, layer_names, FlashInferImpl)
753
+ per_layer_parameters = get_per_layer_parameters(
754
+ vllm_config, layer_names, FlashInferImpl
755
)
756
+ if current_platform.is_device_capability(90) and any(
757
+ params.window_left != -1 for params in per_layer_parameters.values()
758
+ ):
759
+ # FlashInfer SM90 sliding-window prefill is not reliable with FP8-Q:
760
+ # https://github.com/flashinfer-ai/flashinfer/issues/3578
761
+ raise NotImplementedError(
762
+ "FlashInfer backend on SM90 currently crashes with "
763
+ "sliding-window attention layers. Use the default attention "
764
+ "backend."
765
+ )
766
+ self.global_hyperparameters = infer_global_hyperparameters(per_layer_parameters)
767
self.sm_scale = self.global_hyperparameters.sm_scale
768
self.window_left = self.global_hyperparameters.window_left
769
self.logits_soft_cap = self.global_hyperparameters.logits_soft_cap
770
self.has_sinks = self.global_hyperparameters.has_sinks
716
- if self.has_sinks and not can_use_trtllm:
771
+ if self.has_sinks and not FlashInferBackend.supports_sink():
772
raise NotImplementedError(
773
"FlashInfer backend currently does not support attention "
774
"sinks, please use trtllm on blackwell or flash attention on "
775
"earlier GPUs."
776
)
777
+ capability = current_platform.get_device_capability()
778
+ arch = f"sm{capability.major}{capability.minor}" if capability else "unknown"
779
+ decode_backend = (
780
+ self.flashinfer_trtllm_api_decode_kernel.value
781
+ if self.flashinfer_trtllm_api_decode_kernel is not None
782
+ else "flashinfer-native"
783
+ )
784
+ logger.info_once(
785
+ "FlashInfer resolved query dtypes: prefill=%s, decode=%s, "
786
+ "decode_backend=%s, kv_cache_dtype=%s, arch=%s",
787
+ self.q_data_type_prefill,
788
+ self.q_data_type_decode,
789
+ decode_backend,
790
+ self.kv_cache_dtype,
791
+ arch,
792
+ )
793
# Preparing persistent buffers
794
# Since we do not have explicit synchronization in ModelRunnerV2, we do not pin
795
# reused CPU buffers to avoid a race condition between step N async copies to
@@ -731,6 +802,37 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
802
self.paged_kv_indices = self._make_buffer(max_num_pages)
803
self.paged_kv_last_page_len = self._make_buffer(max_num_reqs)
804
805
+ # Keep SM90 prefill/decode Q dtype selection in one place.
806
+ @classmethod
807
+ def get_q_data_type(
808
+ cls,
809
+ vllm_config: VllmConfig,
810
+ kv_cache_spec: AttentionSpec,
811
+ is_prefill: bool,
812
+ ) -> torch.dtype:
813
+ # The user sets --attention-config.disable_flashinfer_q_quantization
814
+ # to 1 explicitly, use model dtype for query.
815
+ if vllm_config.attention_config.disable_flashinfer_q_quantization:
816
+ return vllm_config.model_config.dtype
817
+
818
+ # On SM90, XQA decode requires BF16/FP16-Q even with FP8 KV cache.
819
+ # FI native prefill on SM90 still uses FP8-Q in that case.
820
+ cache_dtype = vllm_config.cache_config.cache_dtype
821
+ if (
822
+ current_platform.is_device_capability(90)
823
+ and not is_prefill
824
+ and force_use_trtllm_attention() is not False
825
+ and cache_dtype.startswith("fp8")
826
+ ):
827
+ return vllm_config.model_config.dtype
828
+
829
+ # Otherwise, match Q dtype to the KV cache dtype.
830
+ if cache_dtype.startswith("fp8"):
831
+ return FlashInferBackend.get_dtype_for_flashinfer(cache_dtype)
832
+ if cache_dtype == "nvfp4":
833
+ return FlashInferBackend.get_dtype_for_flashinfer("fp8_e4m3")
834
+ return kv_cache_spec.dtype
835
+
836
def _make_buffer(
837
self, *size: int | torch.SymInt, dtype: torch.dtype = torch.int32
838
) -> CpuGpuBuffer:
@@ -751,12 +853,13 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
853
) -> AttentionCGSupport:
854
"""Get the cudagraph support level for FlashInfer attention.
855
754
- This depends on whether we can use TRTLLM attention for decodes, since we can
755
- only do UNIFORM_SINGLE_TOKEN_DECODE if it is unavailable.
756
- To check this, we must call can_use_trtllm_attention with the number of KV
757
- heads from the kv_cache_spec. We check all available KV cache specs and
758
- only return UNIFORM_BATCH if all of them support TRTLLM attention.
856
+ The SM90 XQA integration only enables single-token decode today. Keep
857
+ specdec CUDA graphs limited to trtllm-gen until vLLM wires the XQA
858
+ specdec mask.
859
"""
860
+ if current_platform.is_device_capability(90):
861
+ return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
862
+
863
# For UniformTypeKVCacheSpecs, check all contained specs
864
kv_specs = (
865
kv_cache_spec.kv_cache_specs.values()
@@ -775,6 +878,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
878
if not can_use_trtllm_attention(
879
num_qo_heads=num_qo_heads,
880
num_kv_heads=spec.num_kv_heads,
881
+ is_prefill=False,
882
):
883
has_trtllm_support = False
884
break
@@ -797,6 +901,13 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
901
def set_workspace_buffer(self, workspace_buffer: torch.Tensor):
902
self._workspace_buffer = workspace_buffer
903
904
+ @staticmethod
905
+ def _get_flashinfer_trtllm_api_decode_kernel() -> FlashInferDecodeKernel:
906
+ if current_platform.is_device_capability(90):
907
+ return FlashInferDecodeKernel.XQA
908
+ assert current_platform.is_device_capability_family(100)
909
+ return FlashInferDecodeKernel.TRTLLM_GEN
910
+
911
def _get_prefill_wrapper(
912
self,
913
causal: bool = True,
@@ -976,7 +1087,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1087
# Step 1: Decide which dispatch modes to use:
1088
# - Cascade attention (distinct mode)
1089
# - Prefill (FI native or TRTLLM)
979
- # - Decode (FI native or TRTLLM)
1090
+ # - Decode (FI native, XQA, or trtllm-gen)
1091
use_cascade = common_prefix_len > 0
1092
uses_spec_reorder = self.reorder_batch_threshold > 1
1093
# Page sizes >= 128 must use trtllm-gen; force it for prefill too.
@@ -990,13 +1101,13 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1101
max_seq_len,
1102
self.dcp_world_size,
1103
self.cache_dtype,
993
- self.q_data_type,
1104
+ self.q_data_type_prefill,
1105
is_prefill=True,
1106
force_use_trtllm=prefill_force_trtllm,
1107
has_sinks=self.has_sinks,
1108
has_spec=uses_spec_reorder,
1109
)
999
- decode_use_trtllm = (
1110
+ decode_with_flashinfer_trtllm_api = (
1111
causal and self.use_trtllm_decode_attention and self.dcp_world_size <= 1
1112
)
1113
@@ -1011,7 +1122,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1122
)
1123
all_uses_trtllm = causal and (
1124
(num_prefills == 0 or prefill_use_trtllm)
1014
- and (num_decodes == 0 or decode_use_trtllm)
1125
+ and (num_decodes == 0 or decode_with_flashinfer_trtllm_api)
1126
)
1127
1128
if not all_uses_trtllm:
@@ -1035,17 +1146,14 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1146
"`sm_scale`."
1147
)
1148
1038
- # The q quantization is not supported for non-trtllm attention,
1039
- # fall back to model dtype.
1040
- self.q_data_type = self.model_config.dtype
1041
-
1149
# Step 2: Initialize the output metadata
1150
# Leave prefill/decode/cascade_wrapper empty, to be populated
1151
# case by case depending on the batch contents and backend selection.
1152
attn_metadata = FlashInferMetadata(
1153
num_actual_tokens=num_actual_tokens,
1154
slot_mapping=common_attn_metadata.slot_mapping,
1048
- q_data_type=self.q_data_type,
1155
+ q_data_type_prefill=self.q_data_type_prefill,
1156
+ q_data_type_decode=self.q_data_type_decode,
1157
num_decodes=num_decodes,
1158
num_decode_tokens=num_decode_tokens,
1159
num_prefills=num_prefills,
@@ -1101,7 +1209,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1209
1210
# Compute paged_kv_indices if necessary
1211
# paged_kv_indices is only needed for FlashInfer native paths;
1104
- # TRTLLM paths use block_tables directly on GPU.
1212
+ # XQA/trtllm-gen paths use block_tables directly on GPU.
1213
needs_paged_kv_indices = use_cascade or not all_uses_trtllm
1214
if needs_paged_kv_indices:
1215
assert num_blocks_np is not None
@@ -1143,6 +1251,9 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1251
paged_kv_last_page_len_cpu = self.paged_kv_last_page_len.cpu[:num_reqs]
1252
1253
attn_metadata.cascade_wrapper = self._get_cascade_wrapper()
1254
+ # Cascade attention must use the same q dtype for prefill and decode
1255
+ # because it does not support FP8 kv-cache or FP8 query yet.
1256
+ assert self.q_data_type_prefill == self.q_data_type_decode
1257
attn_metadata.cascade_wrapper.plan(
1258
qo_indptr_arr=[shared_qo_indptr_cpu, qo_indptr_cpu],
1259
paged_kv_indptr_arr=[shared_kv_page_indptr_cpu, paged_kv_indptr_cpu],
@@ -1159,7 +1270,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1270
sm_scale=self.sm_scale,
1271
window_left=self.window_left,
1272
logits_soft_cap=self.logits_soft_cap,
1162
- q_data_type=self.q_data_type,
1273
+ q_data_type=self.q_data_type_prefill,
1274
kv_data_type=self.kv_cache_dtype,
1275
)
1276
return attn_metadata
@@ -1233,7 +1344,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1344
sm_scale=self.sm_scale,
1345
window_left=self.window_left,
1346
logits_soft_cap=self.logits_soft_cap,
1236
- q_data_type=self.q_data_type,
1347
+ q_data_type=self.q_data_type_prefill,
1348
kv_cache_dtype=self.kv_cache_dtype,
1349
prefill_fixed_split_size=self.prefill_fixed_split_size,
1350
disable_split_kv=self.disable_split_kv,
@@ -1262,7 +1373,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1373
sm_scale=self.sm_scale,
1374
window_left=self.window_left,
1375
logits_soft_cap=self.logits_soft_cap,
1265
- q_data_type=self.q_data_type,
1376
+ q_data_type=self.q_data_type_prefill,
1377
kv_data_type=self.kv_cache_dtype,
1378
o_data_type=o_dtype,
1379
fixed_split_size=self.prefill_fixed_split_size,
@@ -1272,12 +1383,14 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1383
1384
## DECODE PATHWAY
1385
if num_decodes > 0:
1275
- if decode_use_trtllm:
1386
+ if decode_with_flashinfer_trtllm_api:
1387
assert num_decode_tokens % num_decodes == 0, (
1277
- "TRTLLM decode requires uniform query lengths per request. "
1388
+ "XQA/trtllm-gen decode requires uniform query lengths per request. "
1389
f"Got {num_decode_tokens=} and {num_decodes=}."
1390
)
1280
- attn_metadata.decode = TRTLLMDecode(
1391
+ assert self.flashinfer_trtllm_api_decode_kernel is not None
1392
+ attn_metadata.decode = FlashInferTrtllmAPIDecode(
1393
+ kernel=self.flashinfer_trtllm_api_decode_kernel,
1394
block_tables=block_table_tensor[:num_decodes],
1395
seq_lens=seq_lens[:num_decodes],
1396
max_seq_len=max_seq_len,
@@ -1320,7 +1433,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
1433
sm_scale=self.sm_scale,
1434
window_left=self.window_left,
1435
logits_soft_cap=self.logits_soft_cap,
1323
- q_data_type=self.q_data_type,
1436
+ q_data_type=self.q_data_type_decode,
1437
kv_data_type=self.kv_cache_dtype,
1438
o_data_type=o_dtype,
1439
fixed_split_size=self.decode_fixed_split_size,
@@ -1396,10 +1509,17 @@ class FlashInferImpl(AttentionImpl):
1509
)
1510
self.sinks = sinks
1511
1399
- self.support_trtllm_attn = can_use_trtllm_attention(num_heads, num_kv_heads)
1512
+ self.supports_xqa_or_trtllm_gen_decode = can_use_trtllm_attention(
1513
+ num_heads, num_kv_heads, is_prefill=False
1514
+ )
1515
vllm_config = get_current_vllm_config_or_none()
1516
+ # Query pre-quantization needs a single dtype for the whole query tensor.
1517
+ # SM90 XQA needs BF16/FP16-Q for decode and FP8 for prefill,
1518
+ # so only enable this for SM100 trtllm-gen where both use FP8-Q.
1519
self.supports_quant_query_input = (
1402
- self.support_trtllm_attn
1520
+ self.supports_xqa_or_trtllm_gen_decode
1521
+ and is_quantized_kv_cache(self.kv_cache_dtype)
1522
+ and current_platform.is_device_capability_family(100)
1523
and vllm_config is not None
1524
and not vllm_config.attention_config.disable_flashinfer_q_quantization
1525
)
@@ -1429,9 +1549,12 @@ class FlashInferImpl(AttentionImpl):
1549
self.dcp_combine = partial(cp_lse_ag_out_rs, is_lse_base_on_e=False)
1550
1551
def fused_output_quant_supported(self, quant_key: QuantKey):
1552
+ # XQA does not support FP8/NVFP4 output, so require trtllm-gen
1553
+ # (SM100+) here. Without that we cannot fuse the output quant.
1554
return (
1433
- self.support_trtllm_attn
1555
+ self.supports_xqa_or_trtllm_gen_decode
1556
and is_quantized_kv_cache(self.kv_cache_dtype)
1557
+ and current_platform.is_device_capability_family(100)
1558
and quant_key in (kFp8StaticTensorSym, kNvfp4Dynamic)
1559
)
1560
@@ -1440,6 +1563,37 @@ class FlashInferImpl(AttentionImpl):
1563
if self.sinks is not None and self.sinks.dtype != torch.float32:
1564
self.sinks = self.sinks.to(torch.float32)
1565
1566
+ def get_xqa_bmm1_scale(self, layer: torch.nn.Module, q_data_type: torch.dtype):
1567
+ bmm1_scale = self.scale
1568
+ if is_quantized_kv_cache(self.kv_cache_dtype):
1569
+ if q_data_type in (torch.float8_e4m3fn, torch.float8_e5m2):
1570
+ bmm1_scale *= layer._q_scale_float
1571
+ bmm1_scale *= layer._k_scale_float
1572
+ return bmm1_scale
1573
+
1574
+ # SM90 may need FP8-Q for native prefill and BF16/FP16-Q for XQA decode,
1575
+ # so quantize only the slice whose target dtype differs.
1576
+ def maybe_quant_query(
1577
+ self,
1578
+ query: torch.Tensor,
1579
+ q_data_type: torch.dtype,
1580
+ scale: torch.Tensor,
1581
+ ) -> torch.Tensor:
1582
+ if query.dtype != q_data_type:
1583
+ assert query.dtype in [torch.float16, torch.bfloat16]
1584
+ assert q_data_type in [torch.float8_e4m3fn, torch.float8_e5m2]
1585
+ assert query.is_contiguous()
1586
+ assert query.dim() == 3
1587
+ num_tokens = query.shape[0]
1588
+ num_heads = query.shape[1]
1589
+ head_size = query.shape[2]
1590
+ query_quantized, _ = custom_ops.scaled_fp8_quant(
1591
+ query.view(num_tokens, num_heads * head_size), scale=scale
1592
+ )
1593
+ return query_quantized.view(num_tokens, num_heads, head_size)
1594
+
1595
+ return query
1596
+
1597
def forward(
1598
self,
1599
layer: torch.nn.Module,
@@ -1469,12 +1623,6 @@ class FlashInferImpl(AttentionImpl):
1623
# Profiling run.
1624
return output.fill_(0)
1625
1472
- # Ensure query dtype matches the expected dtype from attention metadata
1473
- assert attn_metadata.q_data_type == query.dtype, (
1474
- f"Query dtype mismatch: expected {attn_metadata.q_data_type}, "
1475
- f"got {query.dtype}"
1476
- )
1477
-
1626
if self.bmm1_scale is None:
1627
self.bmm1_scale = self.scale
1628
if is_quantized_kv_cache(self.kv_cache_dtype):
@@ -1486,7 +1634,14 @@ class FlashInferImpl(AttentionImpl):
1634
self.bmm2_scale *= layer._v_scale_float
1635
1636
prefill_use_trtllm = isinstance(attn_metadata.prefill, TRTLLMPrefill)
1489
- decode_use_trtllm = isinstance(attn_metadata.decode, TRTLLMDecode)
1637
+ decode_kernel = (
1638
+ attn_metadata.decode.kernel
1639
+ if isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode)
1640
+ else None
1641
+ )
1642
+ decode_with_xqa = decode_kernel == FlashInferDecodeKernel.XQA
1643
+ decode_with_trtllm_gen = decode_kernel == FlashInferDecodeKernel.TRTLLM_GEN
1644
+ decode_with_flashinfer_trtllm_api = decode_with_xqa or decode_with_trtllm_gen
1645
1646
# The attn+quant fusion happens when output_scale is provided.
1647
if output_scale is None:
@@ -1494,12 +1649,15 @@ class FlashInferImpl(AttentionImpl):
1649
"output_block_scale is not supported when fusion has not happened"
1650
)
1651
else:
1497
- assert attn_metadata.q_data_type == FP8_DTYPE, (
1498
- "Query must be FP8 when attn+quant fusion happened."
1652
+ assert attn_metadata.q_data_type_prefill == FP8_DTYPE, (
1653
+ "Query must be FP8 when attn+quant fusion happened for prefill."
1654
+ )
1655
+ assert attn_metadata.q_data_type_decode == FP8_DTYPE, (
1656
+ "Query must be FP8 when attn+quant fusion happened for decode."
1657
)
1658
assert (attn_metadata.num_prefills == 0 or prefill_use_trtllm) and (
1501
- attn_metadata.num_decodes == 0 or decode_use_trtllm
1502
- ), "Must use TRT-LLM attn"
1659
+ attn_metadata.num_decodes == 0 or decode_with_trtllm_gen
1660
+ ), "Output quant fusion requires TRTLLM prefill/trtllm-gen decode"
1661
1662
if output.dtype == FP8_DTYPE:
1663
assert output_block_scale is None, (
@@ -1597,6 +1755,13 @@ class FlashInferImpl(AttentionImpl):
1755
prefill_query = query[num_decode_tokens:]
1756
assert prefill_query.shape[0] == num_prefill_tokens
1757
1758
+ # Convert query to the expected dtype for prefill if needed.
1759
+ prefill_query = self.maybe_quant_query(
1760
+ prefill_query,
1761
+ attn_metadata.q_data_type_prefill,
1762
+ layer._q_scale,
1763
+ )
1764
+
1765
if not prefill_use_trtllm:
1766
assert isinstance(attn_metadata.prefill, FIPrefill)
1767
prefill_wrapper = attn_metadata.prefill.wrapper
@@ -1655,6 +1820,7 @@ class FlashInferImpl(AttentionImpl):
1820
prefill_wrapper.run(
1821
prefill_query,
1822
kv_cache_permute,
1823
+ q_scale=layer._q_scale_float,
1824
k_scale=layer._k_scale_float,
1825
v_scale=layer._v_scale_float,
1826
out=out_prefill,
@@ -1673,7 +1839,7 @@ class FlashInferImpl(AttentionImpl):
1839
# degenerate strides on size=1 dims for TMA alignment.
1840
prefill_query = prefill_query.contiguous()
1841
prefill_query = canonicalize_singleton_dim_strides(prefill_query)
1676
- workspace_buffer = _get_trtllm_gen_workspace_buffer()
1842
+ workspace_buffer = _get_trtllm_workspace_buffer()
1843
block_tables_prefill = attn_metadata.prefill.block_tables
1844
seq_lens_prefill = attn_metadata.prefill.seq_lens
1845
@@ -1705,7 +1871,7 @@ class FlashInferImpl(AttentionImpl):
1871
prefill_kv_block_scales = None
1872
if self.is_kvcache_nvfp4:
1873
# NVFP4 trtllm-gen kernel requires FP8 query.
1708
- assert attn_metadata.q_data_type == FP8_DTYPE, (
1874
+ assert attn_metadata.q_data_type_prefill == FP8_DTYPE, (
1875
"NVFP4 KV cache requires FP8 quantized queries for "
1876
"trtllm-gen prefill. Set "
1877
"disable_flashinfer_q_quantization=False."
@@ -1714,7 +1880,7 @@ class FlashInferImpl(AttentionImpl):
1880
mock_block_table = block_tables_prefill
1881
prefill_kv_block_scales = nvfp4_kv_block_scales
1882
elif (
1717
- attn_metadata.q_data_type != FP8_DTYPE
1883
+ attn_metadata.q_data_type_prefill != FP8_DTYPE
1884
and self.kv_cache_dtype.startswith("fp8")
1885
):
1886
# TRTLLM prefill attention does not support BF16 Q
@@ -1738,7 +1904,7 @@ class FlashInferImpl(AttentionImpl):
1904
block_tables_prefill,
1905
layer._k_scale,
1906
layer._v_scale,
1741
- attn_metadata.q_data_type,
1907
+ attn_metadata.q_data_type_prefill,
1908
)
1909
else:
1910
mock_kv_cache = kv_cache_permute
@@ -1773,7 +1939,14 @@ class FlashInferImpl(AttentionImpl):
1939
decode_query = query[:num_decode_tokens]
1940
assert decode_query.shape[0] == num_decode_tokens
1941
1776
- if not decode_use_trtllm:
1942
+ # Convert query to the expected dtype for decode if needed.
1943
+ decode_query = self.maybe_quant_query(
1944
+ decode_query,
1945
+ attn_metadata.q_data_type_decode,
1946
+ layer._q_scale,
1947
+ )
1948
+
1949
+ if not decode_with_flashinfer_trtllm_api:
1950
assert isinstance(attn_metadata.decode, FIDecode)
1951
decode_wrapper = attn_metadata.decode.wrapper
1952
assert decode_wrapper is not None
@@ -1806,6 +1979,7 @@ class FlashInferImpl(AttentionImpl):
1979
decode_wrapper.run(
1980
decode_query,
1981
kv_cache_permute,
1982
+ q_scale=layer._q_scale_float,
1983
k_scale=layer._k_scale_float,
1984
v_scale=layer._v_scale_float,
1985
out=output_tmp,
@@ -1822,6 +1996,7 @@ class FlashInferImpl(AttentionImpl):
1996
decode_wrapper.run(
1997
decode_query,
1998
kv_cache_permute,
1999
+ q_scale=layer._q_scale_float,
2000
k_scale=layer._k_scale_float,
2001
v_scale=layer._v_scale_float,
2002
out=out_decode,
@@ -1831,19 +2006,23 @@ class FlashInferImpl(AttentionImpl):
2006
if needs_fp8_out:
2007
output[:num_decode_tokens].copy_(out_decode.to(output.dtype))
2008
else:
1834
- assert isinstance(attn_metadata.decode, TRTLLMDecode)
2009
+ assert isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode)
2010
# decode_query may be non-contiguous or have degenerate strides
2011
# on size=1 dims. contiguous() ensures memory layout; then
2012
# canonicalize_singleton_dim_strides fixes any remaining
2013
# degenerate strides on size=1 dims for TMA alignment.
2014
decode_query = decode_query.contiguous()
2015
decode_query = canonicalize_singleton_dim_strides(decode_query)
1841
- workspace_buffer = _get_trtllm_gen_workspace_buffer()
2016
+ workspace_buffer = _get_trtllm_workspace_buffer()
2017
block_tables_decode = attn_metadata.decode.block_tables
2018
seq_lens_decode = attn_metadata.decode.seq_lens
2019
1845
- # This path needs to be enabled with VLLM_KV_CACHE_LAYOUT = HND
1846
- assert get_kv_cache_layout() == "HND"
2020
+ # trtllm-gen needs HND layout on SM100. XQA is selected
2021
+ # separately on SM90 and does not use this SM100 layout gate.
2022
+ if decode_with_trtllm_gen:
2023
+ assert get_kv_cache_layout() == "HND"
2024
+ else:
2025
+ assert decode_with_xqa
2026
assert is_strictly_contiguous(decode_query)
2027
assert is_strictly_contiguous(workspace_buffer)
2028
assert is_strictly_contiguous(block_tables_decode)
@@ -1882,6 +2061,19 @@ class FlashInferImpl(AttentionImpl):
2061
else:
2062
q_len_per_req = num_decode_tokens // attn_metadata.num_decodes
2063
2064
+ if decode_with_xqa and q_len_per_req > 1:
2065
+ raise NotImplementedError(
2066
+ "FlashInfer XQA speculative decode is not wired in vLLM yet."
2067
+ )
2068
+
2069
+ # XQA decode can use model-dtype Q with FP8 KV, so only include
2070
+ # q_scale when the decode query is actually FP8.
2071
+ bmm1_scale = (
2072
+ self.get_xqa_bmm1_scale(layer, attn_metadata.q_data_type_decode)
2073
+ if decode_with_xqa
2074
+ else self.bmm1_scale
2075
+ )
2076
+
2077
trtllm_batch_decode_with_kv_cache(
2078
query=decode_query,
2079
kv_cache=(
@@ -1891,12 +2083,14 @@ class FlashInferImpl(AttentionImpl):
2083
block_tables=block_tables_decode,
2084
seq_lens=seq_lens_decode,
2085
max_seq_len=attn_metadata.decode.max_seq_len,
1894
- bmm1_scale=self.bmm1_scale,
2086
+ bmm1_scale=bmm1_scale,
2087
bmm2_scale=self.bmm2_scale,
2088
window_left=self.window_left,
2089
sinks=self.sinks,
2090
o_sf_scale=self.o_sf_scale,
2091
out=out,
2092
+ kv_layout=get_kv_cache_layout(),
2093
+ backend=attn_metadata.decode.kernel.value,
2094
q_len_per_req=q_len_per_req,
2095
kv_cache_sf=(
2096
nvfp4_kv_block_scales if self.is_kvcache_nvfp4 else None