[Attention] Add Triton/FlashInfer composite for multimodal prefix attention (#56305)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Matthew Bonanni committed
Sep 13, 2026 at 10:29 UTC
82a85dc1d2d5b3ad4453eaa1bd7596de888a66b3
15 files changed
+1095
-3
docs/design/attention_backends.md
+49
@@ -56,6 +56,55 @@ llm = LLM(
56
57
## Backend Selection Behavior
58
59
+### Triton/FlashAttention Composite
60
+
61
+On Hopper, `TRITON_FLASH_ATTN` is preferred for compatible multimodal-prefix
62
+configurations. It uses Triton when the current queries need bidirectional
63
+image attention and FlashAttention for causal text prefills and decode, with a
64
+shared KV cache. The causal child must resolve to FA4. This happens
65
+automatically for FA4-only shapes such as head size 512 and models whose
66
+version policy promotes all layers to FA4, including Gemma 4. A standalone
67
+head-size-256 configuration whose version policy selects FA3 falls back to
68
+Triton for the whole layer.
69
+
70
+No attention override is needed for supported multimodal models:
71
+
72
+```bash
73
+vllm serve google/gemma-4-31B-it
74
+```
75
+
76
+### Triton/FlashInfer Composite
77
+
78
+On Blackwell, `TRITON_FLASHINFER` is preferred for compatible
79
+multimodal-prefix configurations, including Gemma 4 with BF16 or FP8 KV cache. It uses
80
+Triton when a batch's current queries require bidirectional image attention,
81
+and FlashInfer for causal text prefills and decode. Historical image tokens
82
+alone do not select Triton.
83
+
84
+This instantiates `create_composite_attention_backend` from
85
+`vllm/v1/attention/backends/composite.py` with Triton,
86
+FlashInfer, and `MMPrefixAttentionRouting`. The reusable factory owns child
87
+implementations, metadata dispatch, compatible cache requirements, and workspace
88
+sharing. The routing policy selects the child and defines graph-capture safety.
89
+Other combinations can reuse the same machinery.
90
+
91
+The backend supports head dimensions 256/512, FP16/BF16 and FP8 KV cache,
92
+and 64-token kernel pages with a head-major cache layout. TRTLLM handles causal
93
+attention at both head dimensions; its hdim512 kernels do not support 128-token
94
+pages.
95
+For Gemma 4, full CUDA graphs cover single-token batches; multi-token batches
96
+use the non-full-graph execution path. Models whose image masks extend beyond
97
+the sliding window cannot use full attention graphs with this composite.
98
+Context parallelism, R-SWA, attention sinks,
99
+and adaptive verification are not supported by this composite.
100
+
101
+It can also be selected explicitly:
102
+
103
+```bash
104
+vllm serve google/gemma-4-31B-it \
105
+ --attention-backend TRITON_FLASHINFER
106
+```
107
+
108
### Manual Selection
109
110
When you explicitly set a backend via `--attention-backend` or `AttentionConfig`:
tests/kernels/attention/test_attention_selector.py
+139
@@ -808,3 +808,142 @@ def test_fa4_hd256_impl_selection(attn_type, sliding_window, expected):
808
)
809
assert impl.vllm_flash_attn_version == expected
810
assert impl.fa4_hd256 == (expected == 4)
811
+
812
+
813
+@pytest.fixture
814
+def blackwell_selection():
815
+ with (
816
+ patch.object(
817
+ type(current_platform),
818
+ "get_device_capability",
819
+ return_value=DeviceCapability(10, 0),
820
+ ),
821
+ patch(
822
+ "vllm.v1.attention.backends.fa_utils.is_fa_version_supported",
823
+ return_value=True,
824
+ ),
825
+ ):
826
+ yield
827
+
828
+
829
+@pytest.fixture
830
+def hopper_selection():
831
+ with (
832
+ patch.object(
833
+ type(current_platform),
834
+ "get_device_capability",
835
+ return_value=DeviceCapability(9, 0),
836
+ ),
837
+ patch(
838
+ "vllm.v1.attention.backends.fa_utils.is_fa_version_supported",
839
+ return_value=True,
840
+ ),
841
+ ):
842
+ yield
843
+
844
+
845
+@blackwell_only
846
+@pytest.mark.parametrize("use_mm_prefix", [False, True])
847
+@pytest.mark.parametrize("flash_attn_version", [None, 3, 4])
848
+def test_hopper_mm_prefix_selects_triton_flash_attn(
849
+ use_mm_prefix, flash_attn_version, hopper_selection
850
+):
851
+ """Hopper selects the composite only when its causal route resolves FA4."""
852
+ from vllm.engine.arg_utils import EngineArgs
853
+
854
+ config = EngineArgs(
855
+ model="google/gemma-4-31B-it",
856
+ dtype="bfloat16",
857
+ attention_config=AttentionConfig(flash_attn_version=flash_attn_version),
858
+ ).create_engine_config()
859
+ with set_current_vllm_config(config):
860
+ backend = get_attn_backend(
861
+ 256, torch.bfloat16, None, use_mm_prefix=use_mm_prefix
862
+ )
863
+ fa4_resolved = config.attention_config.flash_attn_version == 4
864
+ if use_mm_prefix:
865
+ expected = "TRITON_FLASH_ATTN" if fa4_resolved else "TRITON_ATTN"
866
+ else:
867
+ expected = "FLASH_ATTN"
868
+ assert backend.get_name() == expected
869
+
870
+
871
+@blackwell_only
872
+@pytest.mark.parametrize("use_mm_prefix", [False, True])
873
+@pytest.mark.parametrize("kv_cache_dtype", [None, "fp8_e4m3"])
874
+def test_mm_prefix_selects_composite_without_changing_causal_default(
875
+ use_mm_prefix, kv_cache_dtype, blackwell_selection
876
+):
877
+ """The image-mask requirement must reach CUDA's automatic backend priority."""
878
+ from vllm.engine.arg_utils import EngineArgs
879
+
880
+ config = EngineArgs(
881
+ model="google/gemma-4-31B-it", dtype="bfloat16"
882
+ ).create_engine_config()
883
+ with set_current_vllm_config(config):
884
+ backend = get_attn_backend(
885
+ 256, torch.bfloat16, kv_cache_dtype, use_mm_prefix=use_mm_prefix
886
+ )
887
+ expected = "TRITON_FLASHINFER" if use_mm_prefix else "FLASHINFER"
888
+ assert backend.get_name() == expected
889
+
890
+
891
+@blackwell_only
892
+@pytest.mark.parametrize(
893
+ "feature_kwargs,reason",
894
+ [
895
+ ({"use_rswa": True}, "R-SWA not supported"),
896
+ ({"kv_cache_dtype": "int8_per_token_head"}, "kv_cache_dtype not supported"),
897
+ ({"block_size": 16}, "block_size not supported"),
898
+ ({"use_dcp": True}, "DCP not supported"),
899
+ ({"use_pcp": True}, "PCP not supported"),
900
+ ({"use_adaptive_verification": True}, "device-cpu query lens mismatch"),
901
+ ],
902
+)
903
+def test_composite_rejects_features_not_shared_by_both_routes(feature_kwargs, reason):
904
+ """Automatic selection must not admit features only one child can execute."""
905
+ from vllm.engine.arg_utils import EngineArgs
906
+ from vllm.v1.attention.backends.triton_flashinfer import TritonFlashInferBackend
907
+
908
+ config = EngineArgs(
909
+ model="google/gemma-4-31B-it", dtype="bfloat16"
910
+ ).create_engine_config()
911
+ args = dict(
912
+ head_size=256,
913
+ dtype=torch.bfloat16,
914
+ kv_cache_dtype="auto",
915
+ block_size=128,
916
+ use_mla=False,
917
+ has_sink=False,
918
+ use_sparse=False,
919
+ use_mm_prefix=True,
920
+ use_per_head_quant_scales=False,
921
+ device_capability=DeviceCapability(10, 0),
922
+ attn_type=AttentionType.DECODER,
923
+ )
924
+ args.update(feature_kwargs)
925
+ with set_current_vllm_config(config):
926
+ reasons = TritonFlashInferBackend.validate_configuration(**args)
927
+ assert any(reason in message for message in reasons), reasons
928
+
929
+
930
+@blackwell_only
931
+def test_rswa_selection_does_not_reuse_causal_result(blackwell_selection):
932
+ """An R-SWA requirement must invalidate a previously cached causal selection."""
933
+ from vllm.engine.arg_utils import EngineArgs
934
+
935
+ config = EngineArgs(
936
+ model="google/gemma-4-31B-it",
937
+ dtype="bfloat16",
938
+ attention_config={"backend": "TRITON_FLASHINFER"},
939
+ ).create_engine_config()
940
+ with set_current_vllm_config(config):
941
+ assert (
942
+ get_attn_backend(256, torch.bfloat16, None).get_name()
943
+ == "TRITON_FLASHINFER"
944
+ )
945
+ config.model_config.model_arch_config.rswa_window = 128
946
+ with pytest.raises(ValueError, match="R-SWA"):
947
+ get_attn_backend(256, torch.bfloat16, None)
948
+ config.attention_config.backend = AttentionBackendEnum.TRITON_ATTN
949
+ assert get_attn_backend(256, torch.bfloat16, None).get_name() == "TRITON_ATTN"
tests/v1/attention/test_mm_prefix.py
+246
@@ -638,3 +638,249 @@ def test_mm_prefix_kv_cache_path(head_size: int):
638
assert not torch.allclose(expected, causal_only, atol=2e-2, rtol=2e-2), (
639
"test batch does not actually exercise the mm_prefix branch"
640
)
641
+
642
+
643
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA backend imports")
644
+@pytest.mark.parametrize(
645
+ "query_len,seq_len,spans,expected,unclamped_expected",
646
+ [
647
+ (16, 32, [(20, 28)], True, True),
648
+ (16, 32, [(0, 16)], False, True),
649
+ (16, 32, [(0, 17)], True, True),
650
+ (16, 32, [(32, 40)], False, False),
651
+ (16, 32, [(31, 40)], False, True),
652
+ (1, 24, [(20, 28)], False, True),
653
+ (0, 24, [(20, 28)], False, False),
654
+ (16, 32, [(20, 20)], False, False),
655
+ (16, 64, [(20, 28)], False, False),
656
+ ],
657
+)
658
+def test_composite_routes_queries_that_need_image_masking(
659
+ query_len, seq_len, spans, expected, unclamped_expected
660
+):
661
+ """Historical images and single-query steps must use the causal graph path."""
662
+ from types import SimpleNamespace
663
+
664
+ from vllm.v1.attention.backends.composite import requires_mm_prefix
665
+
666
+ common = SimpleNamespace(
667
+ max_query_len=query_len,
668
+ mm_req_doc_ranges={0: spans},
669
+ query_start_loc_cpu=torch.tensor([0, query_len]),
670
+ seq_lens_cpu_upper_bound=torch.tensor([seq_len]),
671
+ )
672
+ assert requires_mm_prefix(common) is expected
673
+ assert requires_mm_prefix(common, unclamped_window=True) is unclamped_expected
674
+
675
+
676
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA attention kernels")
677
+@pytest.mark.parametrize("head_size", [256, 512])
678
+@pytest.mark.parametrize(
679
+ "reverse_children,cache_dtype",
680
+ [(False, "auto"), (True, "auto"), (False, "fp8_e4m3")],
681
+)
682
+@pytest.mark.parametrize("causal_backend_name", ["FLASHINFER", "FLASH_ATTN"])
683
+def test_composite_shared_cache_across_image_and_causal_steps(
684
+ head_size, reverse_children, cache_dtype, causal_backend_name
685
+):
686
+ """Changing routes must preserve KV writes, image masking and graph replay."""
687
+ if causal_backend_name == "FLASHINFER":
688
+ if not current_platform.is_device_capability_family(100):
689
+ pytest.skip("The Triton/FlashInfer composite supports Blackwell")
690
+ else:
691
+ if not current_platform.is_device_capability(90):
692
+ pytest.skip("The Triton/FlashAttention composite supports Hopper")
693
+ if reverse_children or cache_dtype != "auto":
694
+ pytest.skip("Reverse-child and FP8 coverage is backend-independent")
695
+
696
+ from vllm.config import set_current_vllm_config
697
+ from vllm.engine.arg_utils import EngineArgs
698
+ from vllm.model_executor.layers.attention import Attention
699
+ from vllm.utils.torch_utils import set_default_torch_dtype
700
+ from vllm.v1.attention.backend import AttentionCGSupport, CommonAttentionMetadata
701
+ from vllm.v1.attention.backends.composite import (
702
+ MMPrefixAttentionRouting,
703
+ create_composite_attention_backend,
704
+ )
705
+ from vllm.v1.attention.backends.flash_attn import (
706
+ FlashAttentionMetadata,
707
+ )
708
+ from vllm.v1.attention.backends.flashinfer import (
709
+ FlashInferBackend,
710
+ FlashInferMetadata,
711
+ FlashInferTrtllmAPIDecode,
712
+ TRTLLMPrefill,
713
+ )
714
+ from vllm.v1.attention.backends.triton_attn import (
715
+ TritonAttentionBackend,
716
+ TritonAttentionMetadata,
717
+ )
718
+ from vllm.v1.attention.backends.triton_flash_attn import (
719
+ TritonFlashAttentionBackend,
720
+ )
721
+ from vllm.v1.attention.backends.triton_flashinfer import (
722
+ TritonFlashInferBackend,
723
+ )
724
+ from vllm.v1.kv_cache_interface import FullAttentionSpec, SlidingWindowSpec
725
+
726
+ if causal_backend_name == "FLASHINFER":
727
+ backend = TritonFlashInferBackend
728
+ causal_metadata_type = FlashInferMetadata
729
+ block_size = backend.get_supported_kernel_block_sizes()[0]
730
+ else:
731
+ backend = TritonFlashAttentionBackend
732
+ causal_metadata_type = FlashAttentionMetadata
733
+ block_size = backend.get_preferred_block_size(16)
734
+ num_blocks = (209 + block_size - 1) // block_size
735
+ torch.manual_seed(42)
736
+ attention_config = {"backend": backend.get_name()}
737
+ if causal_backend_name == "FLASH_ATTN":
738
+ attention_config["flash_attn_version"] = 4
739
+ cfg = EngineArgs(
740
+ model="google/gemma-4-31B-it",
741
+ dtype="bfloat16",
742
+ max_model_len=512,
743
+ max_num_seqs=4,
744
+ max_num_batched_tokens=512,
745
+ block_size=block_size,
746
+ kv_cache_dtype=cache_dtype,
747
+ enforce_eager=True,
748
+ attention_config=attention_config,
749
+ ).create_engine_config()
750
+ cfg.cache_config.kv_cache_layout = "LBHNC"
751
+ window = 128 if head_size == 256 else None
752
+ storage_dtype = torch.uint8 if cache_dtype == "fp8_e4m3" else DTYPE
753
+ spec_kwargs = dict(
754
+ block_size=block_size,
755
+ num_kv_heads=16,
756
+ head_size=head_size,
757
+ dtype=storage_dtype,
758
+ kv_quant_mode=get_kv_quant_mode(cache_dtype),
759
+ )
760
+ spec = (
761
+ SlidingWindowSpec(**spec_kwargs, sliding_window=window)
762
+ if window
763
+ else FullAttentionSpec(**spec_kwargs)
764
+ )
765
+ if reverse_children:
766
+ # Swapping the children and policy must preserve outputs and KV writes;
767
+ # dispatch cannot depend on a particular child's class or position.
768
+ class ReversedRouting(MMPrefixAttentionRouting):
769
+ capture_variant = 0
770
+
771
+ def select(self, metadata):
772
+ return 1 - super().select(metadata)
773
+
774
+ @staticmethod
775
+ def variant_uses_mm_prefix(variant):
776
+ return variant == 1
777
+
778
+ backend = create_composite_attention_backend(
779
+ FlashInferBackend,
780
+ TritonAttentionBackend,
781
+ name="ReversedCompositeBackend",
782
+ backend_name="CUSTOM",
783
+ module=__name__,
784
+ routing_policy=ReversedRouting,
785
+ )
786
+ with set_current_vllm_config(cfg), set_default_torch_dtype(DTYPE):
787
+ layer = Attention(
788
+ 32,
789
+ head_size,
790
+ head_size**-0.5,
791
+ num_kv_heads=16,
792
+ cache_config=cfg.cache_config,
793
+ prefix="composite_test",
794
+ per_layer_sliding_window=window,
795
+ attn_backend=backend,
796
+ ).to(DEVICE)
797
+ layer.mm_prefix_clamp_sliding_window = window is not None
798
+ assert backend.get_builder_cls().get_cudagraph_support(cfg, spec) == (
799
+ AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
800
+ )
801
+ if window:
802
+ layer.mm_prefix_clamp_sliding_window = False
803
+ assert backend.get_builder_cls().get_cudagraph_support(cfg, spec) == (
804
+ AttentionCGSupport.NEVER
805
+ )
806
+ layer.mm_prefix_clamp_sliding_window = True
807
+ builder = backend.get_builder_cls()(spec, ["composite_test"], cfg, DEVICE)
808
+ builder.set_kernel_block_size(block_size)
809
+ # Match production, where unwritten page tails are zero-initialized.
810
+ cache = torch.zeros(
811
+ num_blocks,
812
+ 16,
813
+ block_size,
814
+ 2 * head_size,
815
+ dtype=storage_dtype,
816
+ device=DEVICE,
817
+ )
818
+ keys = torch.randn(209, 16, head_size, dtype=DTYPE, device=DEVICE)
819
+ values = torch.randn_like(keys)
820
+ if cache_dtype == "fp8_e4m3":
821
+ keys = keys.to(torch.float8_e4m3fn).to(DTYPE)
822
+ values = values.to(torch.float8_e4m3fn).to(DTYPE)
823
+ table = torch.arange(num_blocks, dtype=torch.int32, device=DEVICE).unsqueeze(0)
824
+ previous = 0
825
+ for end, spans, metadata_type in [
826
+ (128, [(32, 111)], TritonAttentionMetadata),
827
+ (129, [(32, 111)], causal_metadata_type),
828
+ (145, [(32, 111)], causal_metadata_type),
829
+ (209, [(32, 111), (160, 207)], TritonAttentionMetadata),
830
+ ]:
831
+ qlen = end - previous
832
+ starts = torch.tensor([0, qlen], dtype=torch.int32)
833
+ lengths = torch.tensor([end], dtype=torch.int32)
834
+ common = CommonAttentionMetadata(
835
+ query_start_loc=starts.to(DEVICE),
836
+ query_start_loc_cpu=starts,
837
+ seq_lens=lengths.to(DEVICE),
838
+ seq_lens_cpu_upper_bound=lengths,
839
+ num_reqs=1,
840
+ num_actual_tokens=qlen,
841
+ max_query_len=qlen,
842
+ max_seq_len=end,
843
+ block_table_tensor=table,
844
+ slot_mapping=torch.arange(previous, end, device=DEVICE),
845
+ mm_req_doc_ranges={0: spans},
846
+ )
847
+ metadata = builder.build(0, common)
848
+ assert isinstance(metadata, metadata_type)
849
+ if isinstance(metadata, FlashInferMetadata):
850
+ if qlen == 1:
851
+ assert isinstance(metadata.decode, FlashInferTrtllmAPIDecode)
852
+ else:
853
+ assert isinstance(metadata.prefill, TRTLLMPrefill)
854
+ query = torch.randn(qlen, 32, head_size, dtype=DTYPE, device=DEVICE)
855
+ if cache_dtype == "fp8_e4m3":
856
+ query = query.to(torch.float8_e4m3fn).to(DTYPE)
857
+ key, value = keys[previous:end], values[previous:end]
858
+ output = torch.empty_like(query)
859
+ layer.impl.do_kv_cache_update(layer, key, value, cache, common.slot_mapping)
860
+ layer.impl.forward(layer, query, key, value, cache, metadata, output)
861
+ expected = _dense_reference(
862
+ query,
863
+ [keys[:end]],
864
+ [values[:end]],
865
+ [qlen],
866
+ [end],
867
+ {0: spans},
868
+ window,
869
+ head_size**-0.5,
870
+ mm_clamp_sw=window or 0,
871
+ )
872
+ torch.testing.assert_close(output.float(), expected, atol=2e-2, rtol=2e-2)
873
+ if qlen == 1:
874
+ metadata = builder.build_for_cudagraph_capture(common)
875
+ assert isinstance(metadata, metadata_type)
876
+ graph = torch.cuda.CUDAGraph()
877
+ with torch.cuda.graph(graph):
878
+ layer.impl.forward(
879
+ layer, query, key, value, cache, metadata, output
880
+ )
881
+ output.zero_()
882
+ graph.replay()
883
+ torch.testing.assert_close(
884
+ output.float(), expected, atol=2e-2, rtol=2e-2
885
+ )
886
+ previous = end
tests/v1/spec_decode/test_dflash2.py
+1
@@ -159,6 +159,7 @@ def test_dflash2_model_decoder_layer_cls(monkeypatch):
159
model_config=SimpleNamespace(
160
dtype=torch.float32,
161
is_mm_prefix_lm=False,
162
+ rswa_window=None,
163
),
164
kernel_config=SimpleNamespace(
165
linear_backend="auto",
vllm/platforms/cuda.py
+8
@@ -87,6 +87,7 @@ def _get_backend_priorities(
87
kv_cache_dtype: CacheDType | None = None,
88
use_non_causal: bool = False,
89
head_size: int | None = None,
90
+ use_mm_prefix: bool = False,
91
) -> list[AttentionBackendEnum]:
92
"""Get backend priorities with lazy import to avoid circular dependency."""
93
from vllm.utils.torch_utils import is_quantized_kv_cache
@@ -156,6 +157,7 @@ def _get_backend_priorities(
157
# So prefer FlashAttention when non-causal on SM100f.
158
if device_capability.major == 10 and not use_non_causal:
159
return [
160
+ *([AttentionBackendEnum.TRITON_FLASHINFER] if use_mm_prefix else []),
161
AttentionBackendEnum.FLASHINFER,
162
AttentionBackendEnum.FLASH_ATTN,
163
AttentionBackendEnum.TRITON_ATTN,
@@ -164,6 +166,11 @@ def _get_backend_priorities(
166
]
167
else:
168
return [
169
+ *(
170
+ [AttentionBackendEnum.TRITON_FLASH_ATTN]
171
+ if device_capability.major == 9 and use_mm_prefix
172
+ else []
173
+ ),
174
AttentionBackendEnum.FLASH_ATTN,
175
AttentionBackendEnum.FLASHINFER,
176
AttentionBackendEnum.TRITON_ATTN,
@@ -394,6 +401,7 @@ class CudaPlatformBase(Platform):
401
kv_cache_dtype=attn_selector_config.kv_cache_dtype,
402
use_non_causal=attn_selector_config.use_non_causal,
403
head_size=attn_selector_config.head_size,
404
+ use_mm_prefix=attn_selector_config.use_mm_prefix,
405
)
406
for priority, backend in enumerate(backend_priorities):
407
try:
vllm/v1/attention/backend.py
+10
@@ -185,6 +185,10 @@ class AttentionBackend(ABC):
185
def supports_sliding_window(cls) -> bool:
186
return False
187
188
+ @classmethod
189
+ def supports_rswa(cls) -> bool:
190
+ return False
191
+
192
@classmethod
193
def supports_non_causal(cls) -> bool:
194
"""Check if backend supports non-causal (bidirectional) attention
@@ -287,6 +291,7 @@ class AttentionBackend(ABC):
291
use_pcp: bool = False,
292
use_adaptive_verification: bool = False,
293
use_dcp: bool = False,
294
+ use_rswa: bool = False,
295
) -> list[str]:
296
invalid_reasons = []
297
if not cls.supports_head_size(head_size):
@@ -321,6 +326,8 @@ class AttentionBackend(ABC):
326
invalid_reasons.append(f"attention type {attn_type} not supported")
327
if has_sliding_window and not cls.supports_sliding_window():
328
invalid_reasons.append("sliding window not supported")
329
+ if use_rswa and not cls.supports_rswa():
330
+ invalid_reasons.append("R-SWA not supported")
331
if use_non_causal and not cls.supports_non_causal():
332
invalid_reasons.append("non-causal attention not supported")
333
if use_mla and use_non_causal and use_dcp and not cls.supports_non_causal_dcp():
@@ -854,6 +861,9 @@ class AttentionImplBase(ABC, Generic[T]):
861
def process_weights_after_loading(self, act_dtype: torch.dtype):
862
pass
863
864
+ def get_impl_variants(self) -> tuple["AttentionImplBase", ...]:
865
+ return (self,)
866
+
867
def prepare_for_batch(self, attn_metadata: T | None) -> None:
868
"""Prepare implementation-specific state for the current batch."""
869
vllm/v1/attention/backends/composite.py
new
+509
@@ -0,0 +1,509 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Reusable attention backend composition and routing policies."""
4
+
5
+import functools
6
+import math
7
+from typing import ClassVar, Protocol, cast
8
+
9
+import torch
10
+
11
+from vllm.config import VllmConfig
12
+from vllm.v1.attention.backend import (
13
+ AttentionBackend,
14
+ AttentionCGSupport,
15
+ AttentionImpl,
16
+ AttentionMetadataBuilder,
17
+ CommonAttentionMetadata,
18
+)
19
+from vllm.v1.attention.backends.utils import get_supported_kv_cache_layouts
20
+from vllm.v1.kv_cache_interface import KVCacheSpec
21
+
22
+
23
+def requires_mm_prefix(
24
+ metadata: CommonAttentionMetadata, *, unclamped_window: bool = False
25
+) -> bool:
26
+ ranges = metadata.mm_req_doc_ranges
27
+ # With one query per request there are no future keys to unmask. This
28
+ # also keeps single-token prefills on the captured FlashInfer path.
29
+ if not ranges or (metadata.max_query_len <= 1 and not unclamped_window):
30
+ return False
31
+ seq_lens = metadata.seq_lens_cpu_upper_bound
32
+ assert seq_lens is not None
33
+ starts = metadata.query_start_loc_cpu
34
+ for req_idx, spans in ranges.items():
35
+ query_len = int(starts[req_idx + 1] - starts[req_idx])
36
+ if query_len == 0 or (query_len == 1 and not unclamped_window):
37
+ continue
38
+ end = int(seq_lens[req_idx])
39
+ begin = end - query_len
40
+ # An unclamped image can also expose keys behind the sliding window.
41
+ if unclamped_window and any(
42
+ start < stop and start < end and stop >= begin for start, stop in spans
43
+ ):
44
+ return True
45
+ # Prefill lengths are exact. Historical image ranges must not divert
46
+ # later text/decode queries to the image backend.
47
+ if any(
48
+ start < stop and start < end - 1 and stop > begin for start, stop in spans
49
+ ):
50
+ return True
51
+ return False
52
+
53
+
54
+def _has_unclamped_window(layers) -> bool:
55
+ return any(
56
+ getattr(layer, "sliding_window", None) is not None
57
+ and not getattr(layer, "mm_prefix_clamp_sliding_window", False)
58
+ for layer in layers
59
+ )
60
+
61
+
62
+class CompositeAttentionRouting(Protocol):
63
+ """Select a child backend and declare routing constraints for composition."""
64
+
65
+ capture_variant: ClassVar[int]
66
+ """Child used during graph capture; replay must select the same child."""
67
+
68
+ def __init__(self, layer_names: list[str], vllm_config: VllmConfig) -> None: ...
69
+
70
+ def select(self, metadata: CommonAttentionMetadata) -> int:
71
+ """Return 0 for the first child or 1 for the second child."""
72
+ ...
73
+
74
+ @staticmethod
75
+ def get_cudagraph_support(
76
+ vllm_config: VllmConfig, kv_cache_spec: KVCacheSpec
77
+ ) -> AttentionCGSupport:
78
+ """Limit graph support to batches whose routing stays invariant."""
79
+ ...
80
+
81
+ @staticmethod
82
+ def variant_uses_mm_prefix(variant: int) -> bool:
83
+ """Whether this child must support the model's image-mask requirement."""
84
+ ...
85
+
86
+
87
+class MMPrefixAttentionRouting(CompositeAttentionRouting):
88
+ """Route image masks to variant zero and causal queries to variant one."""
89
+
90
+ def __init__(self, layer_names: list[str], vllm_config: VllmConfig) -> None:
91
+ layers = vllm_config.compilation_config.static_forward_context
92
+ self.unclamped_window = _has_unclamped_window(
93
+ layers[name] for name in layer_names
94
+ )
95
+
96
+ def select(self, metadata: CommonAttentionMetadata) -> int:
97
+ return int(
98
+ not requires_mm_prefix(metadata, unclamped_window=self.unclamped_window)
99
+ )
100
+
101
+ capture_variant: ClassVar[int] = 1
102
+
103
+ @staticmethod
104
+ def get_cudagraph_support(
105
+ vllm_config: VllmConfig, kv_cache_spec: KVCacheSpec
106
+ ) -> AttentionCGSupport:
107
+ if _has_unclamped_window(
108
+ vllm_config.compilation_config.static_forward_context.values()
109
+ ):
110
+ return AttentionCGSupport.NEVER
111
+ return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
112
+
113
+ @staticmethod
114
+ def variant_uses_mm_prefix(variant: int) -> bool:
115
+ return variant == 0
116
+
117
+
118
+def _intersect_kernel_block_sizes(
119
+ first: type[AttentionBackend], second: type[AttentionBackend]
120
+):
121
+ from vllm.v1.attention.backend import MultipleOf
122
+
123
+ result: list[int | MultipleOf] = []
124
+ seen: set[tuple[type, int]] = set()
125
+ for lhs in first.get_supported_kernel_block_sizes():
126
+ for rhs in second.get_supported_kernel_block_sizes():
127
+ candidate: int | MultipleOf | None = None
128
+ if isinstance(lhs, MultipleOf) and isinstance(rhs, MultipleOf):
129
+ candidate = MultipleOf(math.lcm(lhs.base, rhs.base))
130
+ elif isinstance(lhs, MultipleOf) and isinstance(rhs, int):
131
+ candidate = rhs if rhs % lhs.base == 0 else None
132
+ elif isinstance(lhs, int) and isinstance(rhs, MultipleOf):
133
+ candidate = lhs if lhs % rhs.base == 0 else None
134
+ elif lhs == rhs:
135
+ candidate = lhs
136
+ if candidate is None:
137
+ continue
138
+ value = candidate.base if isinstance(candidate, MultipleOf) else candidate
139
+ key = (type(candidate), value)
140
+ if key not in seen:
141
+ result.append(candidate)
142
+ seen.add(key)
143
+ return result
144
+
145
+
146
+@functools.cache
147
+def create_composite_attention_backend(
148
+ general_backend: type[AttentionBackend],
149
+ causal_backend: type[AttentionBackend],
150
+ *,
151
+ name: str,
152
+ backend_name: str,
153
+ module: str,
154
+ routing_policy: type[CompositeAttentionRouting],
155
+ head_sizes: tuple[int, ...] = (),
156
+ kernel_block_sizes: tuple[int, ...] = (),
157
+ device_major: int | None = None,
158
+) -> type[AttentionBackend]:
159
+ """Compose two backends with shared storage and an explicit routing policy."""
160
+ if general_backend.full_cls_name() == causal_backend.full_cls_name():
161
+ return general_backend
162
+ general_impl_cls = general_backend.get_impl_cls()
163
+ causal_impl_cls = causal_backend.get_impl_cls()
164
+ general_builder_cls = general_backend.get_builder_cls()
165
+ causal_builder_cls = causal_backend.get_builder_cls()
166
+ if (
167
+ general_backend.forward_includes_kv_cache_update
168
+ or causal_backend.forward_includes_kv_cache_update
169
+ ):
170
+ raise ValueError("Composite backends require separate KV cache updates")
171
+
172
+ class CompositeAttentionImpl(AttentionImpl):
173
+ def __init__(self, *args, **kwargs) -> None:
174
+ self.general_impl = cast(AttentionImpl, general_impl_cls(*args, **kwargs))
175
+ try:
176
+ self.causal_impl = cast(AttentionImpl, causal_impl_cls(*args, **kwargs))
177
+ except TypeError as e:
178
+ raise ValueError(
179
+ f"Causal attention backend {causal_backend.get_name()} "
180
+ f"does not accept the attention arguments required by "
181
+ f"the general backend {general_backend.get_name()}: {e}"
182
+ ) from e
183
+ for name in (
184
+ "num_heads",
185
+ "num_kv_heads",
186
+ "head_size",
187
+ "scale",
188
+ "kv_cache_dtype",
189
+ ):
190
+ if hasattr(self.general_impl, name):
191
+ setattr(self, name, getattr(self.general_impl, name))
192
+ self.supports_quant_query_input = (
193
+ self.general_impl.supports_quant_query_input
194
+ and self.causal_impl.supports_quant_query_input
195
+ )
196
+ self.supports_pcp = self.general_impl.supports_pcp
197
+ self.supports_dcp = self.causal_impl.supports_dcp
198
+ self.can_return_lse_for_decode = self.causal_impl.can_return_lse_for_decode
199
+ self.lse_base_on_e = self.causal_impl.lse_base_on_e
200
+ self.need_to_return_lse_for_decode = (
201
+ self.causal_impl.need_to_return_lse_for_decode
202
+ )
203
+ self.supports_mtp_with_cp_non_trivial_interleave_size = (
204
+ self.causal_impl.supports_mtp_with_cp_non_trivial_interleave_size
205
+ )
206
+
207
+ def get_impl_for_metadata(self, attn_metadata):
208
+ return (
209
+ self.causal_impl
210
+ if getattr(attn_metadata, "_attention_backend_variant", 0)
211
+ else self.general_impl
212
+ )
213
+
214
+ def get_impl_variants(self):
215
+ return self.general_impl, self.causal_impl
216
+
217
+ def forward(
218
+ self,
219
+ layer,
220
+ query,
221
+ key,
222
+ value,
223
+ kv_cache,
224
+ attn_metadata,
225
+ output,
226
+ output_scale=None,
227
+ output_block_scale=None,
228
+ ):
229
+ impl = self.get_impl_for_metadata(attn_metadata)
230
+ return impl.forward(
231
+ layer,
232
+ query,
233
+ key,
234
+ value,
235
+ kv_cache,
236
+ attn_metadata,
237
+ output,
238
+ output_scale,
239
+ output_block_scale,
240
+ )
241
+
242
+ def process_weights_after_loading(self, act_dtype: torch.dtype):
243
+ for impl in self.get_impl_variants():
244
+ impl.process_weights_after_loading(act_dtype)
245
+
246
+ def fused_output_quant_supported(self, quant_key):
247
+ return all(
248
+ impl.fused_output_quant_supported(quant_key)
249
+ for impl in self.get_impl_variants()
250
+ )
251
+
252
+ def fused_qk_norm_rope_kvcache_supported(self):
253
+ return self.general_impl.fused_qk_norm_rope_kvcache_supported()
254
+
255
+ def do_qk_norm_rope_kvcache_update(self, *args, **kwargs):
256
+ method = self.general_impl.do_qk_norm_rope_kvcache_update
257
+ return method(*args, **kwargs)
258
+
259
+ def fused_rope_kvcache_supported(self):
260
+ return self.general_impl.fused_rope_kvcache_supported()
261
+
262
+ def do_rope_and_kv_cache_update(self, *args, **kwargs):
263
+ method = self.general_impl.do_rope_and_kv_cache_update
264
+ return method(*args, **kwargs)
265
+
266
+ def do_kv_cache_update(self, *args, **kwargs):
267
+ method = self.general_impl.do_kv_cache_update # type: ignore[attr-defined]
268
+ return method(*args, **kwargs)
269
+
270
+ class CompositeAttentionMetadataBuilder(AttentionMetadataBuilder):
271
+ requires_block_table_width = (
272
+ general_builder_cls.requires_block_table_width
273
+ or causal_builder_cls.requires_block_table_width
274
+ )
275
+
276
+ def __init__(self, kv_cache_spec, layer_names, vllm_config, device, **kwargs):
277
+ super().__init__(kv_cache_spec, layer_names, vllm_config, device)
278
+ args = (kv_cache_spec, layer_names, vllm_config, device)
279
+ # Only the variant that asked for the extra ctor kwargs receives them.
280
+ self.general_builder = general_builder_cls(
281
+ *args,
282
+ **(kwargs if general_builder_cls.requires_block_table_width else {}),
283
+ )
284
+ self.causal_builder = causal_builder_cls(
285
+ *args,
286
+ **(kwargs if causal_builder_cls.requires_block_table_width else {}),
287
+ )
288
+ self.reorder_batch_threshold = min(
289
+ (
290
+ builder.reorder_batch_threshold
291
+ for builder in self._builders
292
+ if builder.reorder_batch_threshold is not None
293
+ ),
294
+ default=None,
295
+ )
296
+ self.routing = routing_policy(layer_names, vllm_config)
297
+
298
+ def set_kernel_block_size(self, kernel_block_size):
299
+ super().set_kernel_block_size(kernel_block_size)
300
+ for builder in self._builders:
301
+ builder.set_kernel_block_size(kernel_block_size)
302
+
303
+ @classmethod
304
+ def get_cudagraph_support(cls, vllm_config, kv_cache_spec):
305
+ return min(
306
+ general_builder_cls.get_cudagraph_support(vllm_config, kv_cache_spec),
307
+ causal_builder_cls.get_cudagraph_support(vllm_config, kv_cache_spec),
308
+ routing_policy.get_cudagraph_support(vllm_config, kv_cache_spec),
309
+ key=lambda support: support.value,
310
+ )
311
+
312
+ def _builder(self, common_attn_metadata):
313
+ backend_variant = self.routing.select(common_attn_metadata)
314
+ builder = self.causal_builder if backend_variant else self.general_builder
315
+ return builder, backend_variant
316
+
317
+ def _build(self, method, common_attn_metadata, **kwargs):
318
+ builder, backend_variant = self._builder(common_attn_metadata)
319
+ metadata = getattr(builder, method)(
320
+ common_attn_metadata=common_attn_metadata, **kwargs
321
+ )
322
+ metadata._attention_backend_variant = backend_variant
323
+ return metadata
324
+
325
+ def build(self, common_prefix_len, common_attn_metadata, fast_build=False):
326
+ return self._build(
327
+ "build",
328
+ common_attn_metadata,
329
+ common_prefix_len=common_prefix_len,
330
+ fast_build=fast_build,
331
+ )
332
+
333
+ def build_for_cudagraph_capture(self, common_attn_metadata):
334
+ variant = routing_policy.capture_variant
335
+ metadata = self._builders[variant].build_for_cudagraph_capture(
336
+ common_attn_metadata
337
+ )
338
+ metadata._attention_backend_variant = variant
339
+ return metadata
340
+
341
+ @property
342
+ def _builders(self):
343
+ return self.general_builder, self.causal_builder
344
+
345
+ # Forward the optional workspace protocol so that a wrapped builder
346
+ # which allocates one (FlashInfer) still joins the runner's cross-group
347
+ # sharing instead of allocating a second buffer behind the composite.
348
+ def _get_workspace_buffer(self):
349
+ for builder in self._builders:
350
+ if hasattr(builder, "_get_workspace_buffer"):
351
+ workspace = builder._get_workspace_buffer()
352
+ # The runner only calls the getter for the group that
353
+ # provides the buffer, so hand it to the sibling here.
354
+ self.set_workspace_buffer(workspace)
355
+ return workspace
356
+ return None
357
+
358
+ def set_workspace_buffer(self, workspace_buffer):
359
+ for builder in self._builders:
360
+ if hasattr(builder, "set_workspace_buffer"):
361
+ builder.set_workspace_buffer(workspace_buffer)
362
+
363
+ class CompositeAttentionBackend(AttentionBackend):
364
+ forward_includes_kv_cache_update = False
365
+ general_backend_cls = general_backend
366
+ causal_backend_cls = causal_backend
367
+
368
+ @staticmethod
369
+ def get_name():
370
+ return backend_name
371
+
372
+ @staticmethod
373
+ def get_impl_cls():
374
+ return CompositeAttentionImpl
375
+
376
+ @staticmethod
377
+ def get_builder_cls():
378
+ return CompositeAttentionMetadataBuilder
379
+
380
+ @staticmethod
381
+ def get_supported_kernel_block_sizes():
382
+ if kernel_block_sizes:
383
+ return [
384
+ size
385
+ for size in kernel_block_sizes
386
+ if all(
387
+ backend.supports_block_size(size)
388
+ for backend in (general_backend, causal_backend)
389
+ )
390
+ ]
391
+ return _intersect_kernel_block_sizes(general_backend, causal_backend)
392
+
393
+ @classmethod
394
+ def supports_block_size(cls, block_size):
395
+ return bool(
396
+ cls.get_supported_kernel_block_sizes()
397
+ ) and super().supports_block_size(block_size)
398
+
399
+ @classmethod
400
+ def get_supported_head_sizes(cls):
401
+ return list(head_sizes)
402
+
403
+ @classmethod
404
+ def supports_head_size(cls, head_size):
405
+ return (not head_sizes or head_size in head_sizes) and all(
406
+ backend.supports_head_size(head_size)
407
+ for backend in (general_backend, causal_backend)
408
+ )
409
+
410
+ @classmethod
411
+ def supports_compute_capability(cls, capability):
412
+ return (device_major is None or capability.major == device_major) and all(
413
+ backend.supports_compute_capability(capability)
414
+ for backend in (general_backend, causal_backend)
415
+ )
416
+
417
+ @classmethod
418
+ def supports_dtype(cls, dtype):
419
+ return all(
420
+ backend.supports_dtype(dtype)
421
+ for backend in (general_backend, causal_backend)
422
+ )
423
+
424
+ @classmethod
425
+ def supports_kv_cache_dtype(cls, dtype):
426
+ return all(
427
+ backend.supports_kv_cache_dtype(dtype)
428
+ for backend in (general_backend, causal_backend)
429
+ )
430
+
431
+ @classmethod
432
+ def supports_mm_prefix(cls):
433
+ return all(
434
+ not routing_policy.variant_uses_mm_prefix(variant)
435
+ or backend.supports_mm_prefix()
436
+ for variant, backend in enumerate((general_backend, causal_backend))
437
+ )
438
+
439
+ @classmethod
440
+ def supports_sliding_window(cls):
441
+ return all(
442
+ backend.supports_sliding_window()
443
+ for backend in (general_backend, causal_backend)
444
+ )
445
+
446
+ @classmethod
447
+ def supports_rswa(cls):
448
+ return all(
449
+ backend.supports_rswa() for backend in (general_backend, causal_backend)
450
+ )
451
+
452
+ @classmethod
453
+ def supports_pcp(cls):
454
+ return False
455
+
456
+ @classmethod
457
+ def supports_dcp(cls):
458
+ return False
459
+
460
+ @classmethod
461
+ def supports_device_cpu_query_lens_mismatch(cls):
462
+ return False
463
+
464
+ @classmethod
465
+ def supports_combination(
466
+ cls,
467
+ head_size,
468
+ dtype,
469
+ kv_cache_dtype,
470
+ block_size,
471
+ use_mla,
472
+ has_sink,
473
+ use_sparse,
474
+ use_mm_prefix,
475
+ device_capability,
476
+ ):
477
+ for variant, backend in enumerate((general_backend, causal_backend)):
478
+ reason = backend.supports_combination(
479
+ head_size,
480
+ dtype,
481
+ kv_cache_dtype,
482
+ block_size,
483
+ use_mla,
484
+ has_sink,
485
+ use_sparse,
486
+ use_mm_prefix and routing_policy.variant_uses_mm_prefix(variant),
487
+ device_capability,
488
+ )
489
+ if reason:
490
+ return f"{backend.get_name()}: {reason}"
491
+ return None
492
+
493
+ @classmethod
494
+ def customize_spec(cls, spec):
495
+ general_spec = general_backend.customize_spec(spec)
496
+ if general_spec != causal_backend.customize_spec(spec):
497
+ raise ValueError("Composite backends require the same KV cache spec")
498
+ return general_spec
499
+
500
+ @classmethod
501
+ def supported_kv_cache_layouts(cls):
502
+ return tuple(
503
+ get_supported_kv_cache_layouts((general_backend, causal_backend))
504
+ )
505
+
506
+ CompositeAttentionBackend.__name__ = name
507
+ CompositeAttentionBackend.__qualname__ = name
508
+ CompositeAttentionBackend.__module__ = module
509
+ return CompositeAttentionBackend
vllm/v1/attention/backends/flash_attn.py
+4
@@ -129,6 +129,10 @@ class FlashAttentionBackend(AttentionBackend):
129
def get_name() -> str:
130
return "FLASH_ATTN"
131
132
+ @classmethod
133
+ def supports_rswa(cls) -> bool:
134
+ return True
135
+
136
@classmethod
137
def supports_sliding_window(cls) -> bool:
138
return True
vllm/v1/attention/backends/flex_attention.py
+4
@@ -105,6 +105,10 @@ class FlexAttentionBackend(AttentionBackend):
105
def get_name() -> str:
106
return "FLEX_ATTENTION"
107
108
+ @classmethod
109
+ def supports_rswa(cls) -> bool:
110
+ return True
111
+
112
@classmethod
113
def supports_sliding_window(cls) -> bool:
114
return True
vllm/v1/attention/backends/registry.py
+6
@@ -49,6 +49,9 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
49
TRITON_ATTN_DIFFKV = (
50
"vllm.v1.attention.backends.triton_attn_diffkv.TritonAttentionDiffKVBackend"
51
)
52
+ TRITON_FLASH_ATTN = (
53
+ "vllm.v1.attention.backends.triton_flash_attn.TritonFlashAttentionBackend"
54
+ )
55
ROCM_ATTN = "vllm.v1.attention.backends.rocm_attn.RocmAttentionBackend"
56
ROCM_AITER_MLA = "vllm.v1.attention.backends.mla.rocm_aiter_mla.AiterMLABackend"
57
ROCM_AITER_TRITON_MLA = (
@@ -63,6 +66,9 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
66
XPU_MLA_SPARSE = "vllm.v1.attention.backends.mla.xpu_mla_sparse.XPUMLASparseBackend"
67
TORCH_SDPA = "" # this tag is only used for ViT
68
FLASHINFER = "vllm.v1.attention.backends.flashinfer.FlashInferBackend"
69
+ TRITON_FLASHINFER = (
70
+ "vllm.v1.attention.backends.triton_flashinfer.TritonFlashInferBackend"
71
+ )
72
FLASHINFER_MLA = (
73
"vllm.v1.attention.backends.mla.flashinfer_mla.FlashInferMLABackend"
74
)
vllm/v1/attention/backends/triton_attn.py
+4
@@ -323,6 +323,10 @@ class TritonAttentionBackend(AttentionBackend):
323
def get_name() -> str:
324
return "TRITON_ATTN"
325
326
+ @classmethod
327
+ def supports_rswa(cls) -> bool:
328
+ return True
329
+
330
@classmethod
331
def supports_sliding_window(cls) -> bool:
332
return True
vllm/v1/attention/backends/triton_flash_attn.py
new
+67
@@ -0,0 +1,67 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Triton image-mask and FlashAttention causal attention composite."""
4
+
5
+import torch
6
+
7
+from vllm.config.cache import CacheDType
8
+from vllm.platforms.interface import DeviceCapability
9
+from vllm.v1.attention.backends.composite import (
10
+ MMPrefixAttentionRouting,
11
+ create_composite_attention_backend,
12
+)
13
+from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
14
+from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
15
+from vllm.v1.attention.backends.triton_attn import TritonAttentionBackend
16
+
17
+
18
+class _FA4Backend(FlashAttentionBackend):
19
+ @classmethod
20
+ def supports_combination(
21
+ cls,
22
+ head_size: int,
23
+ dtype: torch.dtype,
24
+ kv_cache_dtype: CacheDType | None,
25
+ block_size: int | None,
26
+ use_mla: bool,
27
+ has_sink: bool,
28
+ use_sparse: bool,
29
+ use_mm_prefix: bool,
30
+ device_capability: DeviceCapability,
31
+ ) -> str | None:
32
+ reason = super().supports_combination(
33
+ head_size,
34
+ dtype,
35
+ kv_cache_dtype,
36
+ block_size,
37
+ use_mla,
38
+ has_sink,
39
+ use_sparse,
40
+ use_mm_prefix,
41
+ device_capability,
42
+ )
43
+ if reason is not None:
44
+ return reason
45
+ if (
46
+ get_flash_attn_version(
47
+ head_size=head_size,
48
+ has_sinks=has_sink,
49
+ kv_cache_block_size=block_size,
50
+ supports_fa4_hd256=True,
51
+ )
52
+ != 4
53
+ ):
54
+ return "causal route requires FlashAttention v4"
55
+ return None
56
+
57
+
58
+TritonFlashAttentionBackend = create_composite_attention_backend(
59
+ TritonAttentionBackend,
60
+ _FA4Backend,
61
+ name="TritonFlashAttentionBackend",
62
+ backend_name="TRITON_FLASH_ATTN",
63
+ module=__name__,
64
+ routing_policy=MMPrefixAttentionRouting,
65
+ head_sizes=(256, 512),
66
+ device_major=9,
67
+)
vllm/v1/attention/backends/triton_flashinfer.py
new
+22
@@ -0,0 +1,22 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Triton image-mask and FlashInfer causal attention composite."""
4
+
5
+from vllm.v1.attention.backends.composite import (
6
+ MMPrefixAttentionRouting,
7
+ create_composite_attention_backend,
8
+)
9
+from vllm.v1.attention.backends.flashinfer import FlashInferBackend
10
+from vllm.v1.attention.backends.triton_attn import TritonAttentionBackend
11
+
12
+TritonFlashInferBackend = create_composite_attention_backend(
13
+ TritonAttentionBackend,
14
+ FlashInferBackend,
15
+ name="TritonFlashInferBackend",
16
+ backend_name="TRITON_FLASHINFER",
17
+ module=__name__,
18
+ routing_policy=MMPrefixAttentionRouting,
19
+ head_sizes=(256, 512),
20
+ kernel_block_sizes=(64,),
21
+ device_major=10,
22
+)
vllm/v1/attention/backends/utils.py
+19
-2
@@ -8,6 +8,7 @@ from typing import (
8
TYPE_CHECKING,
9
Any,
10
Protocol,
11
+ TypeVar,
12
cast,
13
)
14
@@ -45,6 +46,19 @@ logger = init_logger(__name__)
46
PAD_SLOT_ID = -1
47
NULL_BLOCK_ID = 0
48
49
+
50
+ImplT = TypeVar("ImplT", bound=AttentionImpl)
51
+
52
+
53
+def find_attention_impl_variant(
54
+ impl: AttentionImpl, impl_cls: type[ImplT]
55
+) -> ImplT | None:
56
+ for variant in impl.get_impl_variants():
57
+ if isinstance(variant, impl_cls):
58
+ return cast(ImplT, variant)
59
+ return None
60
+
61
+
62
_LN_2 = math.log(2.0)
63
64
@@ -342,8 +356,8 @@ def get_per_layer_parameters(
356
per_layer_params: dict[str, PerLayerParameters] = {}
357
358
for key, layer in layers.items():
345
- impl = layer.impl
346
- assert isinstance(impl, cls_)
359
+ impl = find_attention_impl_variant(layer.impl, cls_)
360
+ assert impl is not None
361
362
# Infer hyperparameters from the attention layer
363
window_size = getattr(impl, "sliding_window", None)
@@ -1013,6 +1027,9 @@ def create_fast_prefill_custom_backend(
1027
common_attn_metadata.logits_indices_padded
1028
)
1029
self.num_logits_indices = common_attn_metadata.num_logits_indices
1030
+ self._attention_backend_variant = getattr(
1031
+ metadata, "_attention_backend_variant", 0
1032
+ )
1033
1034
return KVSharingFastPrefillAttentionMetadata(metadata, common_attn_metadata)
1035
vllm/v1/attention/selector.py
+7
-1
@@ -36,6 +36,7 @@ class AttentionSelectorConfig(NamedTuple):
36
use_pcp: bool = False
37
use_adaptive_verification: bool = False
38
use_dcp: bool = False
39
+ use_rswa: bool = False
40
41
def __repr__(self):
42
return (
@@ -55,7 +56,8 @@ class AttentionSelectorConfig(NamedTuple):
56
f"use_kv_connector={self.use_kv_connector}, "
57
f"use_adaptive_verification={self.use_adaptive_verification}, "
58
f"use_pcp={self.use_pcp}, "
58
- f"use_dcp={self.use_dcp})"
59
+ f"use_dcp={self.use_dcp}, "
60
+ f"use_rswa={self.use_rswa})"
61
)
62
63
@@ -168,6 +170,10 @@ def get_attn_backend(
170
use_pcp=vllm_config.parallel_config.prefill_context_parallel_size > 1,
171
use_adaptive_verification=use_adaptive_verification,
172
use_dcp=vllm_config.parallel_config.decode_context_parallel_size > 1,
173
+ use_rswa=(
174
+ vllm_config.model_config is not None
175
+ and vllm_config.model_config.rswa_window is not None
176
+ ),
177
)
178
179
# A per-KV-group override (keyed by KVCacheSpecKind) takes precedence over