A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 8, 06:15 UTC
[MM Encoder] Add Triton ViT attention backend (#32183)
Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Isotr0py committed
Feb 15, 2026 at 22:32 UTC
71cd89264f6c88ee4cca7c3cc556885e8844fc92
14 files changed
+178
-51
tests/kernels/attention/test_mha_attn.py
+16
-2
@@ -17,7 +17,7 @@ from vllm.platforms import current_platform
17
from vllm.platforms.cpu import CpuPlatform
18
from vllm.platforms.cuda import CudaPlatform
19
from vllm.platforms.rocm import RocmPlatform
20
-from vllm.utils.torch_utils import set_random_seed
20
+from vllm.utils.torch_utils import set_default_torch_dtype, set_random_seed
21
from vllm.v1.attention.backends.registry import AttentionBackendEnum
22
from vllm.v1.attention.selector import _cached_get_attn_backend
23
@@ -71,6 +71,15 @@ def test_mha_attn_platform(default_vllm_config, device: str):
71
attn = MMEncoderAttention(16, 72, scale=1)
72
assert attn.attn_backend == AttentionBackendEnum.FLASH_ATTN
73
74
+ # Test CUDA with head_size=72 (not divisible by 32)
75
+ # - should use vLLM's FlashAttention
76
+ with (
77
+ patch("vllm.model_executor.models.vision.current_platform", CudaPlatform()),
78
+ set_default_torch_dtype(torch.float32),
79
+ ):
80
+ attn = MMEncoderAttention(16, 72, scale=1)
81
+ assert attn.attn_backend == AttentionBackendEnum.TRITON_ATTN
82
+
83
84
def ref_attention(
85
query: torch.Tensor,
@@ -153,7 +162,12 @@ def test_mha_attn_forward(
162
v,
163
scale=scale,
164
).reshape(batch_size, seq_len, num_heads * head_size)
156
- torch.testing.assert_close(output, ref_output)
165
+ tol_kwargs = (
166
+ dict(rtol=1e-3, atol=1e-3)
167
+ if attn.attn_backend == AttentionBackendEnum.TRITON_ATTN
168
+ else {}
169
+ )
170
+ torch.testing.assert_close(output, ref_output, **tol_kwargs)
171
172
173
@pytest.mark.parametrize("var_seq_len", VAR_SEQ_LENS)
vllm/model_executor/layers/attention/mm_encoder_attention.py
+38
@@ -12,6 +12,7 @@ from vllm.v1.attention.backends.registry import AttentionBackendEnum
12
from vllm.v1.attention.ops.vit_attn_wrappers import (
13
vit_flash_attn_wrapper,
14
vit_torch_sdpa_wrapper,
15
+ vit_triton_attn_wrapper,
16
)
17
18
logger = init_logger(__name__)
@@ -165,6 +166,41 @@ class MMEncoderAttention(CustomOp):
166
output = output.reshape(bsz, q_len, -1)
167
return output
168
169
+ def _forward_triton(
170
+ self,
171
+ query: torch.Tensor,
172
+ key: torch.Tensor,
173
+ value: torch.Tensor,
174
+ cu_seqlens: torch.Tensor | None = None,
175
+ max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
176
+ ) -> torch.Tensor:
177
+ """Input shape:
178
+ (batch_size x seq_len x hidden_size) or
179
+ (batch_size x seq_len x num_heads x head_size)
180
+ """
181
+ assert (cu_seqlens is not None and max_seqlen is not None) or (
182
+ cu_seqlens is None and max_seqlen is None
183
+ ), "cu_seqlens and max_seqlen should be both set or both None."
184
+
185
+ bsz, q_len = query.size()[:2]
186
+ kv_len = key.size(1)
187
+ is_reshaped = query.dim() != 4
188
+
189
+ query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
190
+
191
+ output = vit_triton_attn_wrapper(
192
+ q=query,
193
+ k=key,
194
+ v=value,
195
+ batch_size=bsz,
196
+ scale=self.scale,
197
+ cu_seqlens=cu_seqlens,
198
+ max_seqlen=max_seqlen,
199
+ )
200
+ if is_reshaped:
201
+ output = output.reshape(bsz, q_len, -1)
202
+ return output
203
+
204
def forward_native(
205
self,
206
query: torch.Tensor,
@@ -185,6 +221,8 @@ class MMEncoderAttention(CustomOp):
221
) -> torch.Tensor:
222
if self.is_flash_attn_backend:
223
return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
224
+ elif self.attn_backend == AttentionBackendEnum.TRITON_ATTN:
225
+ return self._forward_triton(query, key, value, cu_seqlens, max_seqlen)
226
elif self.attn_backend == AttentionBackendEnum.TORCH_SDPA:
227
return self._forward_sdpa(query, key, value, cu_seqlens)
228
else:
vllm/model_executor/models/dots_ocr.py
+5
-4
@@ -573,10 +573,11 @@ class DotsVisionTransformer(nn.Module):
573
574
def compute_attn_mask_seqlen(self, cu_seqlens: torch.Tensor) -> int | None:
575
max_seqlen = None
576
- if (
577
- self.attn_backend == AttentionBackendEnum.FLASH_ATTN
578
- or self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
579
- ):
576
+ if self.attn_backend in {
577
+ AttentionBackendEnum.FLASH_ATTN,
578
+ AttentionBackendEnum.ROCM_AITER_FA,
579
+ AttentionBackendEnum.TRITON_ATTN,
580
+ }:
581
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
582
return max_seqlen
583
vllm/model_executor/models/ernie45_vl.py
+5
-4
@@ -446,10 +446,11 @@ class Ernie4_5_VisionTransformer(nn.Module):
446
447
def compute_attn_mask_seqlen(self, cu_seqlens: torch.Tensor) -> torch.Tensor | None:
448
max_seqlen = None
449
- if (
450
- self.attn_backend == AttentionBackendEnum.FLASH_ATTN
451
- or self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
452
- ):
449
+ if self.attn_backend in {
450
+ AttentionBackendEnum.FLASH_ATTN,
451
+ AttentionBackendEnum.ROCM_AITER_FA,
452
+ AttentionBackendEnum.TRITON_ATTN,
453
+ }:
454
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
455
return max_seqlen
456
vllm/model_executor/models/glm4_1v.py
+5
-4
@@ -723,10 +723,11 @@ class Glm4vVisionTransformer(nn.Module):
723
cu_seqlens: torch.Tensor,
724
) -> torch.Tensor | None:
725
max_seqlen = None
726
- if (
727
- self.attn_backend == AttentionBackendEnum.FLASH_ATTN
728
- or self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
729
- ):
726
+ if self.attn_backend in {
727
+ AttentionBackendEnum.FLASH_ATTN,
728
+ AttentionBackendEnum.ROCM_AITER_FA,
729
+ AttentionBackendEnum.TRITON_ATTN,
730
+ }:
731
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
732
return max_seqlen
733
vllm/model_executor/models/paddleocr_vl.py
+2
-8
@@ -730,14 +730,7 @@ class SiglipEncoder(nn.Module):
730
head_size=head_dim,
731
dtype=torch.get_default_dtype(),
732
)
733
- if self.attn_backend not in {
734
- AttentionBackendEnum.FLASH_ATTN,
735
- AttentionBackendEnum.TORCH_SDPA,
736
- AttentionBackendEnum.ROCM_AITER_FA,
737
- }:
738
- raise RuntimeError(
739
- f"PaddleOCR-VL does not support {self.attn_backend} backend now."
740
- )
733
+
734
self.layers = nn.ModuleList(
735
[
736
SiglipEncoderLayer(
@@ -805,6 +798,7 @@ class SiglipEncoder(nn.Module):
798
if self.attn_backend in {
799
AttentionBackendEnum.FLASH_ATTN,
800
AttentionBackendEnum.ROCM_AITER_FA,
801
+ AttentionBackendEnum.TRITON_ATTN,
802
}:
803
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
804
vllm/model_executor/models/qwen2_5_vl.py
+1
-9
@@ -607,15 +607,6 @@ class Qwen2_5_VisionTransformer(nn.Module):
607
dtype=torch.get_default_dtype(),
608
)
609
610
- if self.attn_backend not in {
611
- AttentionBackendEnum.FLASH_ATTN,
612
- AttentionBackendEnum.TORCH_SDPA,
613
- AttentionBackendEnum.ROCM_AITER_FA,
614
- }:
615
- raise RuntimeError(
616
- f"Qwen2.5-VL does not support {self.attn_backend} backend now."
617
- )
618
-
610
with set_model_tag("Qwen2_5_VisionBlock", is_encoder=True):
611
self.blocks = nn.ModuleList(
612
[
@@ -761,6 +752,7 @@ class Qwen2_5_VisionTransformer(nn.Module):
752
if self.attn_backend in {
753
AttentionBackendEnum.FLASH_ATTN,
754
AttentionBackendEnum.ROCM_AITER_FA,
755
+ AttentionBackendEnum.TRITON_ATTN,
756
}:
757
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
758
return max_seqlen
vllm/model_executor/models/qwen2_vl.py
+1
@@ -642,6 +642,7 @@ class Qwen2VisionTransformer(nn.Module):
642
if self.attn_backend in {
643
AttentionBackendEnum.FLASH_ATTN,
644
AttentionBackendEnum.ROCM_AITER_FA,
645
+ AttentionBackendEnum.TRITON_ATTN,
646
}:
647
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
648
return max_seqlen
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+2
@@ -391,6 +391,7 @@ class Qwen3OmniMoeAudioEncoder(nn.Module):
391
if self.attn_backend in {
392
AttentionBackendEnum.FLASH_ATTN,
393
AttentionBackendEnum.ROCM_AITER_FA,
394
+ AttentionBackendEnum.TRITON_ATTN,
395
}:
396
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
397
return max_seqlen
@@ -919,6 +920,7 @@ class Qwen3Omni_VisionTransformer(nn.Module):
920
if self.attn_backend in {
921
AttentionBackendEnum.FLASH_ATTN,
922
AttentionBackendEnum.ROCM_AITER_FA,
923
+ AttentionBackendEnum.TRITON_ATTN,
924
}:
925
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
926
return max_seqlen
vllm/model_executor/models/qwen3_vl.py
+4
-11
@@ -385,14 +385,6 @@ class Qwen3_VisionTransformer(nn.Module):
385
dtype=torch.get_default_dtype(),
386
)
387
388
- if self.attn_backend not in {
389
- AttentionBackendEnum.FLASH_ATTN,
390
- AttentionBackendEnum.TORCH_SDPA,
391
- AttentionBackendEnum.ROCM_AITER_FA,
392
- }:
393
- raise RuntimeError(
394
- f"Qwen3-VL does not support {self.attn_backend} backend now."
395
- )
388
self.blocks = nn.ModuleList(
389
[
390
Qwen3_VisionBlock(
@@ -526,9 +518,10 @@ class Qwen3_VisionTransformer(nn.Module):
518
cu_seqlens: torch.Tensor,
519
) -> torch.Tensor:
520
max_seqlen = torch.zeros([], device=cu_seqlens.device)
529
- if (
530
- self.attn_backend == AttentionBackendEnum.FLASH_ATTN
531
- or self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
521
+ if self.attn_backend in (
522
+ AttentionBackendEnum.FLASH_ATTN,
523
+ AttentionBackendEnum.ROCM_AITER_FA,
524
+ AttentionBackendEnum.TRITON_ATTN,
525
):
526
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
527
return max_seqlen
vllm/model_executor/models/vision.py
+2
-2
@@ -108,7 +108,7 @@ def get_vit_attn_backend(
108
multimodal_config: MultiModalConfig | None = (
109
model_config.multimodal_config if model_config is not None else None
110
)
111
- except AssertionError:
111
+ except (AssertionError, AttributeError):
112
multimodal_config = None
113
114
attn_backend_override = (
@@ -134,7 +134,7 @@ def is_vit_use_data_parallel():
134
multimodal_config: MultiModalConfig | None = (
135
model_config.multimodal_config if model_config is not None else None
136
)
137
- except AssertionError:
137
+ except (AssertionError, AttributeError):
138
multimodal_config = None
139
140
mm_encoder_tp_mode = (
vllm/platforms/cuda.py
+19
-7
@@ -411,8 +411,9 @@ class CudaPlatformBase(Platform):
411
@classmethod
412
def get_supported_vit_attn_backends(cls) -> list["AttentionBackendEnum"]:
413
return [
414
- AttentionBackendEnum.TORCH_SDPA,
414
AttentionBackendEnum.FLASH_ATTN,
415
+ AttentionBackendEnum.TRITON_ATTN,
416
+ AttentionBackendEnum.TORCH_SDPA,
417
]
418
419
@classmethod
@@ -430,14 +431,25 @@ class CudaPlatformBase(Platform):
431
logger.info_once(f"Using backend {backend} for vit attention")
432
return backend
433
433
- # Try FlashAttention first
434
- if (cc := cls.get_device_capability()) and cc.major >= 8:
434
+ cc = cls.get_device_capability()
435
+ for vit_attn_backend in cls.get_supported_vit_attn_backends():
436
+ if vit_attn_backend == AttentionBackendEnum.TORCH_SDPA:
437
+ continue
438
try:
436
- backend_class = AttentionBackendEnum.FLASH_ATTN.get_class()
437
- if backend_class.supports_head_size(
439
+ backend_class = vit_attn_backend.get_class()
440
+ is_backend_supported = backend_class.supports_head_size(
441
head_size
439
- ) and backend_class.supports_dtype(dtype):
440
- return AttentionBackendEnum.FLASH_ATTN
442
+ ) and backend_class.supports_dtype(dtype)
443
+ if cc is not None:
444
+ is_backend_supported = (
445
+ is_backend_supported
446
+ and backend_class.supports_compute_capability(cc)
447
+ )
448
+ if is_backend_supported:
449
+ logger.info_once(
450
+ f"Using backend {vit_attn_backend} for vit attention"
451
+ )
452
+ return vit_attn_backend
453
except ImportError:
454
pass
455
vllm/platforms/rocm.py
+1
@@ -384,6 +384,7 @@ class RocmPlatform(Platform):
384
return [
385
AttentionBackendEnum.FLASH_ATTN,
386
AttentionBackendEnum.ROCM_AITER_FA,
387
+ AttentionBackendEnum.TRITON_ATTN,
388
AttentionBackendEnum.TORCH_SDPA,
389
]
390
vllm/v1/attention/ops/vit_attn_wrappers.py
+77
@@ -110,6 +110,83 @@ def vit_flash_attn_wrapper(
110
)
111
112
113
+def triton_attn_wrapper(
114
+ q: torch.Tensor,
115
+ k: torch.Tensor,
116
+ v: torch.Tensor,
117
+ batch_size: int,
118
+ scale: float | None = None,
119
+ cu_seqlens: torch.Tensor | None = None,
120
+ max_seqlen: torch.Tensor | None = None,
121
+) -> torch.Tensor:
122
+ from vllm.v1.attention.ops.triton_prefill_attention import context_attention_fwd
123
+
124
+ q_len = q.size(1)
125
+ if cu_seqlens is None:
126
+ cu_seqlens = torch.arange(
127
+ 0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=q.device
128
+ )
129
+ max_seqlen = q_len if max_seqlen is None else max_seqlen.item()
130
+
131
+ q, k, v = (einops.rearrange(x, "b s ... -> (b s) ...") for x in [q, k, v])
132
+ output = torch.empty_like(q)
133
+ context_attention_fwd(
134
+ q,
135
+ k,
136
+ v,
137
+ output,
138
+ b_start_loc=cu_seqlens[:-1],
139
+ b_seq_len=cu_seqlens[1:] - cu_seqlens[:-1],
140
+ max_input_len=max_seqlen,
141
+ is_causal=False,
142
+ sliding_window_q=None,
143
+ sliding_window_k=None,
144
+ softmax_scale=scale,
145
+ )
146
+
147
+ context_layer = einops.rearrange(output, "(b s) h d -> b s h d", b=batch_size)
148
+ return context_layer
149
+
150
+
151
+def triton_attn_wrapper_fake(
152
+ q: torch.Tensor,
153
+ k: torch.Tensor,
154
+ v: torch.Tensor,
155
+ batch_size: int,
156
+ scale: float | None = None,
157
+ cu_seqlens: torch.Tensor | None = None,
158
+ max_seqlen: torch.Tensor | None = None,
159
+) -> torch.Tensor:
160
+ return torch.empty_like(q)
161
+
162
+
163
+direct_register_custom_op(
164
+ op_name="triton_attn_wrapper",
165
+ op_func=triton_attn_wrapper,
166
+ fake_impl=triton_attn_wrapper_fake,
167
+)
168
+
169
+
170
+def vit_triton_attn_wrapper(
171
+ q: torch.Tensor,
172
+ k: torch.Tensor,
173
+ v: torch.Tensor,
174
+ batch_size: int,
175
+ scale: float | None = None,
176
+ cu_seqlens: torch.Tensor | None = None,
177
+ max_seqlen: torch.Tensor | None = None,
178
+) -> torch.Tensor:
179
+ return torch.ops.vllm.triton_attn_wrapper(
180
+ q,
181
+ k,
182
+ v,
183
+ batch_size,
184
+ scale,
185
+ cu_seqlens,
186
+ max_seqlen,
187
+ )
188
+
189
+
190
def apply_sdpa(
191
q: torch.Tensor,
192
k: torch.Tensor,