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 14, 05:30 UTC
[MM][Core] Decouple ViT backend from LM backend (#27061)
Signed-off-by: Roger Wang <hey@rogerw.io>
Roger Wang committed
Oct 21, 2025 at 00:30 UTC
c3a2c6ac5f9a13c5c407c69199dd0fa68145acad
16 files changed
+230
-17
tests/config/test_multimodal_config.py
new
+25
@@ -0,0 +1,25 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+
6
+from vllm.attention.backends.registry import _Backend
7
+from vllm.config.multimodal import MultiModalConfig
8
+
9
+
10
+def test_mm_encoder_attn_backend_str_conversion():
11
+ config = MultiModalConfig(mm_encoder_attn_backend="FLASH_ATTN")
12
+ assert config.mm_encoder_attn_backend == _Backend.FLASH_ATTN
13
+
14
+
15
+def test_mm_encoder_attn_backend_invalid():
16
+ with pytest.raises(ValueError):
17
+ MultiModalConfig(mm_encoder_attn_backend="not_a_backend")
18
+
19
+
20
+def test_mm_encoder_attn_backend_hash_updates():
21
+ base_hash = MultiModalConfig().compute_hash()
22
+ overridden_hash = MultiModalConfig(
23
+ mm_encoder_attn_backend=_Backend.FLASH_ATTN
24
+ ).compute_hash()
25
+ assert base_hash != overridden_hash
vllm/attention/layer.py
+10
-1
@@ -16,6 +16,7 @@ from vllm.attention.backends.registry import _Backend, backend_name_to_enum
16
from vllm.attention.selector import get_attn_backend
17
from vllm.attention.utils.kv_sharing_utils import validate_kv_sharing_target
18
from vllm.config import CacheConfig, get_current_vllm_config
19
+from vllm.config.multimodal import MultiModalConfig
20
from vllm.config.vllm import VllmConfig
21
from vllm.distributed.kv_transfer import (
22
get_kv_transfer_group,
@@ -443,6 +444,7 @@ class MultiHeadAttention(nn.Module):
444
# This has no effect, it is only here to make it easier to swap
445
# between Attention and MultiHeadAttention
446
prefix: str = "",
447
+ multimodal_config: MultiModalConfig | None = None,
448
) -> None:
449
super().__init__()
450
self.num_heads = num_heads
@@ -462,7 +464,14 @@ class MultiHeadAttention(nn.Module):
464
dtype = torch.get_default_dtype()
465
466
# Determine the attention backend
465
- backend = get_vit_attn_backend(head_size=head_size, dtype=dtype)
467
+ attn_backend_override = None
468
+ if multimodal_config is not None:
469
+ attn_backend_override = multimodal_config.mm_encoder_attn_backend
470
+ backend = get_vit_attn_backend(
471
+ head_size=head_size,
472
+ dtype=dtype,
473
+ attn_backend_override=attn_backend_override,
474
+ )
475
476
# Some auto-selected backends can be upgraded
477
# to upstream flash attention if available.
vllm/config/model.py
+5
@@ -50,6 +50,7 @@ if TYPE_CHECKING:
50
51
import vllm.model_executor.layers.quantization as me_quant
52
import vllm.model_executor.models as me_models
53
+ from vllm.attention.backends.registry import _Backend
54
from vllm.config.load import LoadConfig
55
from vllm.config.parallel import ParallelConfig
56
from vllm.model_executor.layers.quantization import QuantizationMethods
@@ -57,6 +58,7 @@ if TYPE_CHECKING:
58
else:
59
PretrainedConfig = Any
60
61
+ _Backend = Any
62
me_quant = LazyLoader(
63
"model_executor", globals(), "vllm.model_executor.layers.quantization"
64
)
@@ -307,6 +309,7 @@ class ModelConfig:
309
mm_processor_cache_type: InitVar[MMCacheType | None] = None
310
mm_shm_cache_max_object_size_mb: InitVar[int | None] = None
311
mm_encoder_tp_mode: InitVar[MMEncoderTPMode | None] = None
312
+ mm_encoder_attn_backend: InitVar[_Backend | str | None] = None
313
interleave_mm_strings: InitVar[bool | None] = None
314
skip_mm_profiling: InitVar[bool | None] = None
315
video_pruning_rate: InitVar[float | None] = None
@@ -424,6 +427,7 @@ class ModelConfig:
427
mm_processor_cache_type: MMCacheType | None,
428
mm_shm_cache_max_object_size_mb: int | None,
429
mm_encoder_tp_mode: MMEncoderTPMode | None,
430
+ mm_encoder_attn_backend: _Backend | str | None,
431
interleave_mm_strings: bool | None,
432
skip_mm_profiling: bool | None,
433
video_pruning_rate: float | None,
@@ -733,6 +737,7 @@ class ModelConfig:
737
mm_processor_cache_type=mm_processor_cache_type,
738
mm_shm_cache_max_object_size_mb=mm_shm_cache_max_object_size_mb,
739
mm_encoder_tp_mode=mm_encoder_tp_mode,
740
+ mm_encoder_attn_backend=mm_encoder_attn_backend,
741
interleave_mm_strings=interleave_mm_strings,
742
skip_mm_profiling=skip_mm_profiling,
743
video_pruning_rate=video_pruning_rate,
vllm/config/multimodal.py
+38
-4
@@ -3,13 +3,18 @@
3
4
import hashlib
5
from collections.abc import Mapping
6
-from typing import Any, Literal, TypeAlias
6
+from typing import TYPE_CHECKING, Any, Literal, TypeAlias
7
8
from pydantic import ConfigDict, Field, field_validator, model_validator
9
from pydantic.dataclasses import dataclass
10
11
from vllm.config.utils import config
12
13
+if TYPE_CHECKING:
14
+ from vllm.attention.backends.registry import _Backend
15
+else:
16
+ _Backend = Any
17
+
18
19
@dataclass
20
class BaseDummyOptions:
@@ -112,6 +117,10 @@ class MultiModalConfig:
117
DP (which is controlled by `--data-parallel-size`).
118
This is only supported on a per-model basis and falls back to
119
`"weights"` if the encoder does not support DP."""
120
+ mm_encoder_attn_backend: _Backend | None = None
121
+ """Optional override for the multi-modal encoder attention backend when
122
+ using vision transformers. Accepts any value from
123
+ `vllm.attention.backends.registry._Backend` (e.g. `FLASH_ATTN`)."""
124
interleave_mm_strings: bool = False
125
"""Enable fully interleaved support for multimodal prompts, while using
126
--chat-template-content-format=string."""
@@ -148,6 +157,29 @@ class MultiModalConfig:
157
value[k] = BaseDummyOptions(**v)
158
return value
159
160
+ @field_validator("mm_encoder_attn_backend", mode="before")
161
+ @classmethod
162
+ def _validate_mm_encoder_attn_backend(cls, value: object) -> _Backend | None:
163
+ from vllm.attention.backends.registry import (
164
+ _Backend as BackendEnum,
165
+ )
166
+ from vllm.attention.backends.registry import (
167
+ backend_name_to_enum,
168
+ )
169
+
170
+ if value is None or isinstance(value, BackendEnum):
171
+ return value
172
+
173
+ if isinstance(value, str):
174
+ candidate = backend_name_to_enum(value.upper())
175
+ if candidate is not None:
176
+ return candidate
177
+
178
+ valid_backends = ", ".join(sorted(BackendEnum.__members__.keys()))
179
+ raise ValueError(
180
+ f"Invalid mm encoder attention backend. Expected one of: {valid_backends}."
181
+ )
182
+
183
@model_validator(mode="after")
184
def _validate_multimodal_config(self):
185
if self.mm_processor_cache_type != "shm" and (
@@ -172,9 +204,11 @@ class MultiModalConfig:
204
excluding anything before input ids/embeddings and after
205
the final hidden states.
206
"""
175
- # no factors to consider.
176
- # this config will not affect the computation graph.
177
- factors: list[Any] = []
207
+ factors: list[Any] = [
208
+ self.mm_encoder_attn_backend.name
209
+ if self.mm_encoder_attn_backend is not None
210
+ else None
211
+ ]
212
hash_str = hashlib.md5(str(factors).encode(), usedforsecurity=False).hexdigest()
213
return hash_str
214
vllm/engine/arg_utils.py
+9
@@ -32,6 +32,7 @@ from pydantic.fields import FieldInfo
32
from typing_extensions import TypeIs, deprecated
33
34
import vllm.envs as envs
35
+from vllm.attention.backends.registry import _Backend
36
from vllm.config import (
37
CacheConfig,
38
CompilationConfig,
@@ -451,6 +452,9 @@ class EngineArgs:
452
MultiModalConfig.mm_shm_cache_max_object_size_mb
453
)
454
mm_encoder_tp_mode: MMEncoderTPMode = MultiModalConfig.mm_encoder_tp_mode
455
+ mm_encoder_attn_backend: _Backend | str | None = (
456
+ MultiModalConfig.mm_encoder_attn_backend
457
+ )
458
io_processor_plugin: str | None = None
459
skip_mm_profiling: bool = MultiModalConfig.skip_mm_profiling
460
video_pruning_rate: float = MultiModalConfig.video_pruning_rate
@@ -914,6 +918,10 @@ class EngineArgs:
918
multimodal_group.add_argument(
919
"--mm-encoder-tp-mode", **multimodal_kwargs["mm_encoder_tp_mode"]
920
)
921
+ multimodal_group.add_argument(
922
+ "--mm-encoder-attn-backend",
923
+ **multimodal_kwargs["mm_encoder_attn_backend"],
924
+ )
925
multimodal_group.add_argument(
926
"--interleave-mm-strings", **multimodal_kwargs["interleave_mm_strings"]
927
)
@@ -1160,6 +1168,7 @@ class EngineArgs:
1168
mm_processor_cache_type=self.mm_processor_cache_type,
1169
mm_shm_cache_max_object_size_mb=self.mm_shm_cache_max_object_size_mb,
1170
mm_encoder_tp_mode=self.mm_encoder_tp_mode,
1171
+ mm_encoder_attn_backend=self.mm_encoder_attn_backend,
1172
pooler_config=self.pooler_config,
1173
override_pooler_config=self.override_pooler_config,
1174
logits_processor_pattern=self.logits_processor_pattern,
vllm/model_executor/models/dots_ocr.py
+17
-2
@@ -256,6 +256,7 @@ class DotsVisionAttention(nn.Module):
256
quant_config: QuantizationConfig | None = None,
257
prefix: str = "",
258
use_data_parallel: bool = False,
259
+ attn_backend_override: _Backend | None = None,
260
) -> None:
261
super().__init__()
262
@@ -288,7 +289,9 @@ class DotsVisionAttention(nn.Module):
289
)
290
# Select attention backend
291
self.attn_backend = get_vit_attn_backend(
291
- self.hidden_size_per_attention_head, torch.get_default_dtype()
292
+ self.hidden_size_per_attention_head,
293
+ torch.get_default_dtype(),
294
+ attn_backend_override=attn_backend_override,
295
)
296
self.use_upstream_fa = False
297
@@ -510,6 +513,7 @@ class DotsVisionBlock(nn.Module):
513
quant_config: QuantizationConfig | None = None,
514
prefix: str = "",
515
use_data_parallel: bool = False,
516
+ attn_backend_override: _Backend | None = None,
517
):
518
super().__init__()
519
@@ -521,6 +525,7 @@ class DotsVisionBlock(nn.Module):
525
quant_config=quant_config,
526
prefix=f"{prefix}.attn",
527
use_data_parallel=use_data_parallel,
528
+ attn_backend_override=attn_backend_override,
529
)
530
self.norm1 = RMSNorm(config.embed_dim, eps=config.rms_norm_eps)
531
self.mlp = DotsSwiGLUFFN(
@@ -561,6 +566,7 @@ class DotsVisionTransformer(nn.Module):
566
require_post_norm: bool | None = None,
567
prefix: str = "",
568
use_data_parallel: bool = False,
569
+ attn_backend_override: _Backend | None = None,
570
) -> None:
571
super().__init__()
572
self.config = config
@@ -571,7 +577,9 @@ class DotsVisionTransformer(nn.Module):
577
head_dim = config.embed_dim // config.num_attention_heads
578
self.rotary_pos_emb = VisionRotaryEmbedding(head_dim // 2)
579
self.attn_backend = get_vit_attn_backend(
574
- head_size=head_dim, dtype=torch.get_default_dtype()
580
+ head_size=head_dim,
581
+ dtype=torch.get_default_dtype(),
582
+ attn_backend_override=attn_backend_override,
583
)
584
if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
585
torch.get_default_dtype()
@@ -591,6 +599,7 @@ class DotsVisionTransformer(nn.Module):
599
quant_config=quant_config,
600
prefix=f"{prefix}.blocks.{i}",
601
use_data_parallel=use_data_parallel,
602
+ attn_backend_override=attn_backend_override,
603
)
604
for i in range(num_layers)
605
]
@@ -750,11 +759,17 @@ class DotsOCRForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA
759
self.config.vision_config = vision_config
760
else:
761
vision_config = self.config.vision_config
762
+ attn_backend_override = (
763
+ multimodal_config.mm_encoder_attn_backend
764
+ if multimodal_config is not None
765
+ else None
766
+ )
767
self.vision_tower = DotsVisionTransformer(
768
vision_config,
769
quant_config=self.quant_config,
770
prefix=maybe_prefix(prefix, "vision_tower"),
771
use_data_parallel=self.use_data_parallel,
772
+ attn_backend_override=attn_backend_override,
773
)
774
self.language_model: Qwen2ForCausalLM = init_vllm_registered_model(
775
vllm_config=vllm_config,
vllm/model_executor/models/ernie45_vl.py
+15
-1
@@ -164,6 +164,7 @@ class Ernie4_5_VisionAttention(nn.Module):
164
projection_size: int,
165
quant_config: QuantizationConfig | None = None,
166
prefix: str = "",
167
+ attn_backend_override: _Backend | None = None,
168
) -> None:
169
super().__init__()
170
# Per attention head and per partition values.
@@ -196,6 +197,7 @@ class Ernie4_5_VisionAttention(nn.Module):
197
self.attn_backend = get_vit_attn_backend(
198
head_size=self.hidden_size_per_attention_head,
199
dtype=torch.get_default_dtype(),
200
+ attn_backend_override=attn_backend_override,
201
)
202
203
self.use_upstream_fa = False
@@ -367,6 +369,7 @@ class Ernie4_5_VisionBlock(nn.Module):
369
norm_layer: Callable[[int], nn.Module] | None = None,
370
quant_config: QuantizationConfig | None = None,
371
prefix: str = "",
372
+ attn_backend_override: _Backend | None = None,
373
) -> None:
374
super().__init__()
375
@@ -382,6 +385,7 @@ class Ernie4_5_VisionBlock(nn.Module):
385
projection_size=dim,
386
quant_config=quant_config,
387
prefix=f"{prefix}.attn",
388
+ attn_backend_override=attn_backend_override,
389
)
390
391
self.mlp = Ernie4_5_VisionMLP(
@@ -458,6 +462,7 @@ class Ernie4_5_VisionTransformer(nn.Module):
462
norm_eps: float = 1e-6,
463
quant_config: QuantizationConfig | None = None,
464
prefix: str = "",
465
+ attn_backend_override: _Backend | None = None,
466
) -> None:
467
super().__init__()
468
patch_size = vision_config.patch_size
@@ -493,6 +498,7 @@ class Ernie4_5_VisionTransformer(nn.Module):
498
norm_layer=norm_layer,
499
quant_config=quant_config,
500
prefix=f"{prefix}.blocks.{layer_idx}",
501
+ attn_backend_override=attn_backend_override,
502
)
503
for layer_idx in range(depth)
504
]
@@ -504,7 +510,9 @@ class Ernie4_5_VisionTransformer(nn.Module):
510
self.ln = nn.LayerNorm(hidden_size, eps=1e-6)
511
512
self.attn_backend = get_vit_attn_backend(
507
- head_size=head_dim, dtype=torch.get_default_dtype()
513
+ head_size=head_dim,
514
+ dtype=torch.get_default_dtype(),
515
+ attn_backend_override=attn_backend_override,
516
)
517
if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
518
torch.get_default_dtype()
@@ -1327,11 +1335,17 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1335
self.config = config
1336
self.multimodal_config = multimodal_config
1337
1338
+ attn_backend_override = (
1339
+ multimodal_config.mm_encoder_attn_backend
1340
+ if multimodal_config is not None
1341
+ else None
1342
+ )
1343
self.vision_model = Ernie4_5_VisionTransformer(
1344
config.vision_config,
1345
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1346
quant_config=quant_config,
1347
prefix=maybe_prefix(prefix, "vision_model"),
1348
+ attn_backend_override=attn_backend_override,
1349
)
1350
1351
self.language_model = Ernie4_5_VLMoeForCausalLM(
vllm/model_executor/models/glm4_1v.py
+15
-1
@@ -247,6 +247,7 @@ class Glm4vVisionAttention(nn.Module):
247
quant_config: QuantizationConfig | None = None,
248
prefix: str = "",
249
use_data_parallel: bool = False,
250
+ attn_backend_override: _Backend | None = None,
251
) -> None:
252
super().__init__()
253
# Per attention head and per partition values.
@@ -287,6 +288,7 @@ class Glm4vVisionAttention(nn.Module):
288
self.attn_backend = get_vit_attn_backend(
289
head_size=self.hidden_size_per_attention_head,
290
dtype=torch.get_default_dtype(),
291
+ attn_backend_override=attn_backend_override,
292
)
293
self.use_upstream_fa = False
294
@@ -417,6 +419,7 @@ class Glm4vVisionBlock(nn.Module):
419
quant_config: QuantizationConfig | None = None,
420
prefix: str = "",
421
use_data_parallel: bool = False,
422
+ attn_backend_override: _Backend | None = None,
423
) -> None:
424
super().__init__()
425
if norm_layer is None:
@@ -430,6 +433,7 @@ class Glm4vVisionBlock(nn.Module):
433
quant_config=quant_config,
434
prefix=f"{prefix}.attn",
435
use_data_parallel=use_data_parallel,
436
+ attn_backend_override=attn_backend_override,
437
)
438
self.mlp = Glm4vVisionMLP(
439
dim,
@@ -696,6 +700,7 @@ class Glm4vVisionTransformer(nn.Module):
700
quant_config: QuantizationConfig | None = None,
701
prefix: str = "",
702
use_data_parallel: bool = False,
703
+ attn_backend_override: _Backend | None = None,
704
) -> None:
705
super().__init__()
706
@@ -731,6 +736,7 @@ class Glm4vVisionTransformer(nn.Module):
736
quant_config=quant_config,
737
prefix=f"{prefix}.blocks.{layer_idx}",
738
use_data_parallel=self.use_data_parallel,
739
+ attn_backend_override=attn_backend_override,
740
)
741
for layer_idx in range(depth)
742
]
@@ -759,7 +765,9 @@ class Glm4vVisionTransformer(nn.Module):
765
)
766
767
self.attn_backend = get_vit_attn_backend(
762
- head_size=head_dim, dtype=torch.get_default_dtype()
768
+ head_size=head_dim,
769
+ dtype=torch.get_default_dtype(),
770
+ attn_backend_override=attn_backend_override,
771
)
772
if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
773
torch.get_default_dtype()
@@ -1437,12 +1445,18 @@ class Glm4vForConditionalGeneration(
1445
self.multimodal_config = multimodal_config
1446
self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1447
1448
+ attn_backend_override = (
1449
+ multimodal_config.mm_encoder_attn_backend
1450
+ if multimodal_config is not None
1451
+ else None
1452
+ )
1453
self.visual = Glm4vVisionTransformer(
1454
config.vision_config,
1455
norm_eps=getattr(config, "rms_norm_eps", 1e-5),
1456
quant_config=quant_config,
1457
prefix=maybe_prefix(prefix, "visual"),
1458
use_data_parallel=self.use_data_parallel,
1459
+ attn_backend_override=attn_backend_override,
1460
)
1461
1462
if config.model_type == "glm4v":
vllm/model_executor/models/keye.py
+18
-1
@@ -353,6 +353,7 @@ class KeyeSiglipAttention(nn.Module):
353
config: PretrainedConfig,
354
quant_config: QuantizationConfig | None = None,
355
prefix: str = "",
356
+ attn_backend_override: _Backend | None = None,
357
):
358
super().__init__()
359
self.config = config
@@ -392,7 +393,9 @@ class KeyeSiglipAttention(nn.Module):
393
394
# Detect attention implementation.
395
self.attn_backend = get_vit_attn_backend(
395
- head_size=self.head_dim, dtype=torch.get_default_dtype()
396
+ head_size=self.head_dim,
397
+ dtype=torch.get_default_dtype(),
398
+ attn_backend_override=attn_backend_override,
399
)
400
401
self.use_upstream_fa = False
@@ -521,6 +524,7 @@ class KeyeSiglipEncoderLayer(nn.Module):
524
config: PretrainedConfig,
525
quant_config: QuantizationConfig | None = None,
526
prefix: str = "",
527
+ attn_backend_override: _Backend | None = None,
528
):
529
super().__init__()
530
self.embed_dim = config.hidden_size
@@ -529,6 +533,7 @@ class KeyeSiglipEncoderLayer(nn.Module):
533
config,
534
quant_config=quant_config,
535
prefix=f"{prefix}.self_attn",
536
+ attn_backend_override=attn_backend_override,
537
)
538
self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
539
self.mlp = SiglipMLP(
@@ -573,6 +578,7 @@ class KeyeSiglipEncoder(nn.Module):
578
config: PretrainedConfig,
579
quant_config: QuantizationConfig | None = None,
580
prefix: str = "",
581
+ attn_backend_override: _Backend | None = None,
582
):
583
super().__init__()
584
self.config = config
@@ -585,6 +591,7 @@ class KeyeSiglipEncoder(nn.Module):
591
config,
592
quant_config=quant_config,
593
prefix=f"{prefix}.layers.{layer_idx}",
594
+ attn_backend_override=attn_backend_override,
595
)
596
for layer_idx in range(config.num_hidden_layers)
597
]
@@ -666,6 +673,7 @@ class KeyeSiglipVisionTransformer(nn.Module):
673
config: PretrainedConfig,
674
quant_config: QuantizationConfig | None = None,
675
prefix: str = "",
676
+ attn_backend_override: _Backend | None = None,
677
):
678
super().__init__()
679
self.config = config
@@ -676,6 +684,7 @@ class KeyeSiglipVisionTransformer(nn.Module):
684
config,
685
quant_config=quant_config,
686
prefix=f"{prefix}.encoder",
687
+ attn_backend_override=attn_backend_override,
688
)
689
self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
690
@@ -747,6 +756,7 @@ class KeyeSiglipVisionModel(nn.Module):
756
config: PretrainedConfig,
757
quant_config: QuantizationConfig | None = None,
758
prefix: str = "",
759
+ attn_backend_override: _Backend | None = None,
760
):
761
super().__init__()
762
@@ -754,6 +764,7 @@ class KeyeSiglipVisionModel(nn.Module):
764
config,
765
quant_config=quant_config,
766
prefix=f"{prefix}.vision_model",
767
+ attn_backend_override=attn_backend_override,
768
)
769
self.quant_config = quant_config
770
@@ -1296,10 +1307,16 @@ class BaseKeyeModule(nn.Module):
1307
self.config = config
1308
self.multimodal_config = multimodal_config
1309
1310
+ attn_backend_override = (
1311
+ multimodal_config.mm_encoder_attn_backend
1312
+ if multimodal_config is not None
1313
+ else None
1314
+ )
1315
self.visual = KeyeSiglipVisionModel(
1316
config.vision_config,
1317
quant_config=quant_config,
1318
prefix=maybe_prefix(prefix, "visual"),
1319
+ attn_backend_override=attn_backend_override,
1320
)
1321
1322
self.mlp_AR = self._build_projector(
vllm/model_executor/models/ovis2_5.py
+12
@@ -10,6 +10,7 @@ import torch
10
import torch.nn as nn
11
from transformers import BaseImageProcessor, BatchFeature, PretrainedConfig
12
13
+from vllm.attention.backends.registry import _Backend
14
from vllm.config import VllmConfig
15
from vllm.config.multimodal import BaseDummyOptions
16
from vllm.model_executor.layers.linear import ReplicatedLinear
@@ -105,6 +106,7 @@ class VisualTokenizer(torch.nn.Module):
106
quant_config: QuantizationConfig | None = None,
107
prefix: str = "",
108
use_data_parallel: bool = False,
109
+ attn_backend_override: _Backend | None = None,
110
):
111
super().__init__()
112
self.config = config
@@ -113,6 +115,7 @@ class VisualTokenizer(torch.nn.Module):
115
quant_config=quant_config,
116
prefix=f"{prefix}.vit",
117
use_data_parallel=use_data_parallel,
118
+ attn_backend_override=attn_backend_override,
119
)
120
# reserved tokens for INDICATOR_IDS
121
head_dim = visual_vocab_size - len(INDICATOR_IDS)
@@ -132,6 +135,7 @@ class VisualTokenizer(torch.nn.Module):
135
quant_config: QuantizationConfig | None = None,
136
prefix: str = "",
137
use_data_parallel: bool = False,
138
+ attn_backend_override: _Backend | None = None,
139
):
140
model_type = config.model_type
141
if model_type == "siglip2_navit":
@@ -140,6 +144,7 @@ class VisualTokenizer(torch.nn.Module):
144
quant_config=quant_config,
145
prefix=prefix,
146
use_data_parallel=use_data_parallel,
147
+ attn_backend_override=attn_backend_override,
148
)
149
raise ValueError(f"Unsupported visual tokenizer model_type: {model_type}")
150
@@ -457,6 +462,7 @@ class Ovis2_5(nn.Module, SupportsMultiModal, SupportsPP):
462
super().__init__()
463
config = vllm_config.model_config.hf_config
464
quant_config = vllm_config.quant_config
465
+ multimodal_config = vllm_config.model_config.multimodal_config
466
467
self.config: PretrainedConfig = config
468
self.llm = init_vllm_registered_model(
@@ -464,11 +470,17 @@ class Ovis2_5(nn.Module, SupportsMultiModal, SupportsPP):
470
prefix=maybe_prefix(prefix, "llm"),
471
)
472
473
+ attn_backend_override = (
474
+ multimodal_config.mm_encoder_attn_backend
475
+ if multimodal_config is not None
476
+ else None
477
+ )
478
self.visual_tokenizer = VisualTokenizer(
479
config=config.vit_config,
480
visual_vocab_size=config.visual_vocab_size,
481
quant_config=quant_config,
482
prefix=f"{prefix}.visual_tokenizer",
483
+ attn_backend_override=attn_backend_override,
484
)
485
486
self.vte = VisualEmbedding(config.visual_vocab_size, config.hidden_size)
vllm/model_executor/models/qwen2_5_vl.py
+10
-1
@@ -637,6 +637,7 @@ class Qwen2_5_VisionTransformer(nn.Module):
637
quant_config: QuantizationConfig | None = None,
638
prefix: str = "",
639
use_data_parallel: bool = False,
640
+ attn_backend_override: _Backend | None = None,
641
) -> None:
642
super().__init__()
643
@@ -669,7 +670,9 @@ class Qwen2_5_VisionTransformer(nn.Module):
670
671
use_upstream_fa = False
672
self.attn_backend = get_vit_attn_backend(
672
- head_size=head_dim, dtype=torch.get_default_dtype()
673
+ head_size=head_dim,
674
+ dtype=torch.get_default_dtype(),
675
+ attn_backend_override=attn_backend_override,
676
)
677
if (
678
self.attn_backend != _Backend.FLASH_ATTN
@@ -1226,12 +1229,18 @@ class Qwen2_5_VLForConditionalGeneration(
1229
if multimodal_config.get_limit_per_prompt(
1230
"image"
1231
) or multimodal_config.get_limit_per_prompt("video"):
1232
+ attn_backend_override = (
1233
+ multimodal_config.mm_encoder_attn_backend
1234
+ if multimodal_config is not None
1235
+ else None
1236
+ )
1237
self.visual = Qwen2_5_VisionTransformer(
1238
config.vision_config,
1239
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1240
quant_config=self.quant_config,
1241
prefix=maybe_prefix(prefix, "visual"),
1242
use_data_parallel=self.use_data_parallel,
1243
+ attn_backend_override=attn_backend_override,
1244
)
1245
else:
1246
self.visual = None
vllm/model_executor/models/qwen2_vl.py
+15
-1
@@ -320,6 +320,7 @@ class Qwen2VisionAttention(nn.Module):
320
quant_config: QuantizationConfig | None = None,
321
prefix: str = "",
322
use_data_parallel: bool = False,
323
+ attn_backend_override: _Backend | None = None,
324
) -> None:
325
super().__init__()
326
# Per attention head and per partition values.
@@ -355,6 +356,7 @@ class Qwen2VisionAttention(nn.Module):
356
self.attn_backend = get_vit_attn_backend(
357
head_size=self.hidden_size_per_attention_head,
358
dtype=torch.get_default_dtype(),
359
+ attn_backend_override=attn_backend_override,
360
)
361
self.use_upstream_fa = False
362
@@ -497,6 +499,7 @@ class Qwen2VisionBlock(nn.Module):
499
quant_config: QuantizationConfig | None = None,
500
prefix: str = "",
501
use_data_parallel: bool = False,
502
+ attn_backend_override: _Backend | None = None,
503
) -> None:
504
super().__init__()
505
if norm_layer is None:
@@ -512,6 +515,7 @@ class Qwen2VisionBlock(nn.Module):
515
quant_config=quant_config,
516
prefix=f"{prefix}.attn",
517
use_data_parallel=use_data_parallel,
518
+ attn_backend_override=attn_backend_override,
519
)
520
self.mlp = Qwen2VisionMLP(
521
dim,
@@ -662,6 +666,7 @@ class Qwen2VisionTransformer(nn.Module):
666
quant_config: QuantizationConfig | None = None,
667
prefix: str = "",
668
use_data_parallel: bool = False,
669
+ attn_backend_override: _Backend | None = None,
670
) -> None:
671
super().__init__()
672
@@ -703,6 +708,7 @@ class Qwen2VisionTransformer(nn.Module):
708
quant_config=quant_config,
709
prefix=f"{prefix}.blocks.{layer_idx}",
710
use_data_parallel=use_data_parallel,
711
+ attn_backend_override=attn_backend_override,
712
)
713
for layer_idx in range(depth)
714
]
@@ -716,7 +722,9 @@ class Qwen2VisionTransformer(nn.Module):
722
use_data_parallel=use_data_parallel,
723
)
724
self.attn_backend = get_vit_attn_backend(
719
- head_size=head_dim, dtype=torch.get_default_dtype()
725
+ head_size=head_dim,
726
+ dtype=torch.get_default_dtype(),
727
+ attn_backend_override=attn_backend_override,
728
)
729
if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
730
torch.get_default_dtype()
@@ -1356,12 +1364,18 @@ class Qwen2VLForConditionalGeneration(
1364
if multimodal_config.get_limit_per_prompt(
1365
"image"
1366
) or multimodal_config.get_limit_per_prompt("video"):
1367
+ attn_backend_override = (
1368
+ multimodal_config.mm_encoder_attn_backend
1369
+ if multimodal_config is not None
1370
+ else None
1371
+ )
1372
self.visual = Qwen2VisionTransformer(
1373
config.vision_config,
1374
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1375
quant_config=quant_config,
1376
prefix=maybe_prefix(prefix, "visual"),
1377
use_data_parallel=self.use_data_parallel,
1378
+ attn_backend_override=attn_backend_override,
1379
)
1380
else:
1381
self.visual = None
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+10
-1
@@ -296,6 +296,7 @@ class Qwen3Omni_VisionTransformer(nn.Module):
296
norm_eps: float = 1e-6,
297
quant_config: QuantizationConfig | None = None,
298
prefix: str = "",
299
+ attn_backend_override: _Backend | None = None,
300
) -> None:
301
super().__init__()
302
self.hidden_size = vision_config.hidden_size
@@ -367,7 +368,9 @@ class Qwen3Omni_VisionTransformer(nn.Module):
368
)
369
370
self.attn_backend = get_vit_attn_backend(
370
- head_size=head_dim, dtype=torch.get_default_dtype()
371
+ head_size=head_dim,
372
+ dtype=torch.get_default_dtype(),
373
+ attn_backend_override=attn_backend_override,
374
)
375
if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
376
torch.get_default_dtype()
@@ -1144,11 +1147,17 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
1147
1148
self.audio_tower = Qwen3OmniMoeAudioEncoder(thinker_config.audio_config)
1149
1150
+ attn_backend_override = (
1151
+ multimodal_config.mm_encoder_attn_backend
1152
+ if multimodal_config is not None
1153
+ else None
1154
+ )
1155
self.visual = Qwen3Omni_VisionTransformer(
1156
vision_config=thinker_config.vision_config,
1157
norm_eps=getattr(thinker_config.text_config, "rms_norm_eps", 1e-6),
1158
quant_config=quant_config,
1159
prefix=maybe_prefix(prefix, "visual"),
1160
+ attn_backend_override=attn_backend_override,
1161
)
1162
self.quant_config = quant_config
1163
vllm/model_executor/models/qwen3_vl.py
+10
-2
@@ -300,6 +300,7 @@ class Qwen3_VisionTransformer(nn.Module):
300
quant_config: QuantizationConfig | None = None,
301
prefix: str = "",
302
use_data_parallel: bool = False,
303
+ attn_backend_override: _Backend | None = None,
304
) -> None:
305
super().__init__()
306
self.hidden_size = vision_config.hidden_size
@@ -359,7 +360,9 @@ class Qwen3_VisionTransformer(nn.Module):
360
)
361
362
self.attn_backend = get_vit_attn_backend(
362
- head_size=head_dim, dtype=torch.get_default_dtype()
363
+ head_size=head_dim,
364
+ dtype=torch.get_default_dtype(),
365
+ attn_backend_override=attn_backend_override,
366
)
367
use_upstream_fa = False
368
if (
@@ -379,7 +382,6 @@ class Qwen3_VisionTransformer(nn.Module):
382
raise RuntimeError(
383
f"Qwen3-VL does not support {self.attn_backend} backend now."
384
)
382
-
385
self.blocks = nn.ModuleList(
386
[
387
Qwen3_VisionBlock(
@@ -1214,12 +1216,18 @@ class Qwen3VLForConditionalGeneration(
1216
) and not multimodal_config.get_limit_per_prompt("video"):
1217
self.visual = None
1218
else:
1219
+ attn_backend_override = (
1220
+ multimodal_config.mm_encoder_attn_backend
1221
+ if multimodal_config is not None
1222
+ else None
1223
+ )
1224
self.visual = Qwen3_VisionTransformer(
1225
config.vision_config,
1226
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1227
quant_config=quant_config,
1228
prefix=maybe_prefix(prefix, "visual"),
1229
use_data_parallel=self.use_data_parallel,
1230
+ attn_backend_override=attn_backend_override,
1231
)
1232
1233
self.language_model = Qwen3LLMForCausalLM(
vllm/model_executor/models/siglip2navit.py
+12
-1
@@ -208,6 +208,7 @@ class Siglip2Attention(nn.Module):
208
quant_config: QuantizationConfig | None = None,
209
prefix: str = "",
210
use_data_parallel: bool = False,
211
+ attn_backend_override: _Backend | None = None,
212
):
213
super().__init__()
214
self.config = config
@@ -248,7 +249,9 @@ class Siglip2Attention(nn.Module):
249
250
# Detect attention implementation.
251
self.attn_backend = get_vit_attn_backend(
251
- head_size=self.head_dim, dtype=torch.get_default_dtype()
252
+ head_size=self.head_dim,
253
+ dtype=torch.get_default_dtype(),
254
+ attn_backend_override=attn_backend_override,
255
)
256
self.use_upstream_fa = False
257
@@ -372,6 +375,7 @@ class Siglip2EncoderLayer(nn.Module):
375
quant_config: QuantizationConfig | None = None,
376
prefix: str = "",
377
use_data_parallel: bool = False,
378
+ attn_backend_override: _Backend | None = None,
379
):
380
super().__init__()
381
self.embed_dim = config.hidden_size
@@ -381,6 +385,7 @@ class Siglip2EncoderLayer(nn.Module):
385
quant_config=quant_config,
386
prefix=f"{prefix}.self_attn",
387
use_data_parallel=use_data_parallel,
388
+ attn_backend_override=attn_backend_override,
389
)
390
self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
391
self.mlp = Siglip2MLP(
@@ -434,6 +439,7 @@ class Siglip2Encoder(nn.Module):
439
quant_config: QuantizationConfig | None = None,
440
prefix: str = "",
441
use_data_parallel: bool = False,
442
+ attn_backend_override: _Backend | None = None,
443
):
444
super().__init__()
445
self.config = config
@@ -444,6 +450,7 @@ class Siglip2Encoder(nn.Module):
450
quant_config=quant_config,
451
prefix=f"{prefix}.layers.{idx}",
452
use_data_parallel=use_data_parallel,
453
+ attn_backend_override=attn_backend_override,
454
)
455
for idx in range(config.num_hidden_layers)
456
]
@@ -618,6 +625,7 @@ class Siglip2VisionTransformer(nn.Module):
625
quant_config: QuantizationConfig | None = None,
626
prefix: str = "",
627
use_data_parallel: bool = False,
628
+ attn_backend_override: _Backend | None = None,
629
):
630
super().__init__()
631
self.config = config
@@ -629,6 +637,7 @@ class Siglip2VisionTransformer(nn.Module):
637
quant_config=quant_config,
638
prefix=f"{prefix}.encoder",
639
use_data_parallel=use_data_parallel,
640
+ attn_backend_override=attn_backend_override,
641
)
642
self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
643
@@ -657,6 +666,7 @@ class Siglip2NavitModel(torch.nn.Module):
666
quant_config: QuantizationConfig | None = None,
667
prefix: str = "",
668
use_data_parallel: bool = False,
669
+ attn_backend_override: _Backend | None = None,
670
):
671
super().__init__()
672
@@ -665,6 +675,7 @@ class Siglip2NavitModel(torch.nn.Module):
675
quant_config=quant_config,
676
prefix=f"{prefix}.vision_model",
677
use_data_parallel=use_data_parallel,
678
+ attn_backend_override=attn_backend_override,
679
)
680
681
def forward(
vllm/model_executor/models/vision.py
+9
-1
@@ -78,10 +78,18 @@ def get_vision_encoder_info(hf_config: VisionLanguageConfig) -> VisionEncoderInf
78
raise NotImplementedError(msg)
79
80
81
-def get_vit_attn_backend(head_size: int, dtype: torch.dtype) -> _Backend:
81
+def get_vit_attn_backend(
82
+ head_size: int,
83
+ dtype: torch.dtype,
84
+ *,
85
+ attn_backend_override: _Backend | None = None,
86
+) -> _Backend:
87
"""
88
Get the available attention backend for Vision Transformer.
89
"""
90
+ if attn_backend_override is not None:
91
+ return attn_backend_override
92
+
93
# Lazy import to avoid circular dependency
94
from vllm.attention.selector import get_env_variable_attn_backend
95