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 11, 21:15 UTC
[v1] Add PrefixLM support to FlexAttention backend (#27938)
Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Isotr0py committed
Dec 7, 2025 at 23:51 UTC
b952f4d3c31068de3a98d680261be9a5caa04caa
16 files changed
+173
-25
docs/models/supported_models.md
-20
@@ -740,23 +740,6 @@ Some models are supported only via the [Transformers modeling backend](#transfor
740
<sup>E</sup> Pre-computed embeddings can be inputted for this modality.
741
<sup>+</sup> Multiple items can be inputted per text prompt for this modality.
742
743
-!!! warning
744
- Both V0 and V1 support `Gemma3ForConditionalGeneration` for text-only inputs.
745
- However, there are differences in how they handle text + image inputs:
746
-
747
- V0 correctly implements the model's attention pattern:
748
- - Uses bidirectional attention between the image tokens corresponding to the same image
749
- - Uses causal attention for other tokens
750
- - Implemented via (naive) PyTorch SDPA with masking tensors
751
- - Note: May use significant memory for long prompts with image
752
-
753
- V1 currently uses a simplified attention pattern:
754
- - Uses causal attention for all tokens, including image tokens
755
- - Generates reasonable outputs but does not match the original model's attention for text + image inputs, especially when `{"do_pan_and_scan": true}`
756
- - Will be updated in the future to support the correct behavior
757
-
758
- This limitation exists because the model's mixed attention pattern (bidirectional for images, causal otherwise) is not yet supported by vLLM's attention backends.
759
-
743
!!! note
744
`Gemma3nForConditionalGeneration` is only supported on V1 due to shared KV caching and it depends on `timm>=1.0.17` to make use of its
745
MobileNet-v5 vision backbone.
@@ -776,9 +759,6 @@ Some models are supported only via the [Transformers modeling backend](#transfor
759
The official `openbmb/MiniCPM-V-2` doesn't work yet, so we need to use a fork (`HwwwH/MiniCPM-V-2`) for now.
760
For more details, please see: <https://github.com/vllm-project/vllm/pull/4087#issuecomment-2250397630>
761
779
-!!! warning
780
- Our PaliGemma implementations have the same problem as Gemma 3 (see above) for both V0 and V1.
781
-
762
!!! note
763
For Qwen2.5-Omni and Qwen3-Omni, reading audio from video pre-processing (`--mm-processor-kwargs '{"use_audio_in_video": true}'`) is currently work in progress and not yet supported.
764
tests/models/multimodal/generation/test_common.py
-1
@@ -382,7 +382,6 @@ VLM_TEST_SETTINGS = {
382
auto_cls=AutoModelForImageTextToText,
383
vllm_runner_kwargs={"mm_processor_kwargs": {"do_pan_and_scan": True}},
384
patch_hf_runner=model_utils.gemma3_patch_hf_runner,
385
- num_logprobs=10,
385
),
386
"glm4v": VLMTestInfo(
387
models=["zai-org/glm-4v-9b"],
tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_platform.py
+1
@@ -30,5 +30,6 @@ class DummyPlatform(Platform):
30
use_mla,
31
has_sink,
32
use_sparse,
33
+ use_mm_prefix,
34
):
35
return "vllm_add_dummy_platform.dummy_attention_backend.DummyAttentionBackend" # noqa E501
vllm/attention/backends/abstract.py
+9
@@ -166,6 +166,10 @@ class AttentionBackend(ABC):
166
def supports_sink(cls) -> bool:
167
return False
168
169
+ @classmethod
170
+ def supports_mm_prefix(cls) -> bool:
171
+ return False
172
+
173
@classmethod
174
def is_sparse(cls) -> bool:
175
return False
@@ -207,6 +211,7 @@ class AttentionBackend(ABC):
211
use_mla: bool,
212
has_sink: bool,
213
use_sparse: bool,
214
+ use_mm_prefix: bool,
215
device_capability: "DeviceCapability",
216
attn_type: str,
217
) -> list[str]:
@@ -219,6 +224,10 @@ class AttentionBackend(ABC):
224
invalid_reasons.append("kv_cache_dtype not supported")
225
if not cls.supports_block_size(block_size):
226
invalid_reasons.append("block_size not supported")
227
+ if use_mm_prefix and not cls.supports_mm_prefix():
228
+ invalid_reasons.append(
229
+ "partial multimodal token full attention not supported"
230
+ )
231
if use_mla != cls.is_mla():
232
if use_mla:
233
invalid_reasons.append("MLA not supported")
vllm/attention/layer.py
+5
@@ -230,6 +230,10 @@ class Attention(nn.Module, AttentionLayerBase):
230
self.sliding_window = sliding_window
231
self.has_sink = extra_impl_args.get("sinks") is not None
232
233
+ # NOTE: model_config may be None during certain tests
234
+ model_config = vllm_config.model_config
235
+ self.use_mm_prefix = model_config is not None and model_config.is_mm_prefix_lm
236
+
237
# During model initialization, the default dtype is set as the model
238
# weight and activation dtype.
239
dtype = torch.get_default_dtype()
@@ -241,6 +245,7 @@ class Attention(nn.Module, AttentionLayerBase):
245
block_size,
246
use_mla=False,
247
has_sink=self.has_sink,
248
+ use_mm_prefix=self.use_mm_prefix,
249
attn_type=attn_type,
250
)
251
else:
vllm/attention/selector.py
+5
@@ -27,6 +27,7 @@ def get_attn_backend(
27
use_mla: bool = False,
28
has_sink: bool = False,
29
use_sparse: bool = False,
30
+ use_mm_prefix: bool = False,
31
attn_type: str | None = None,
32
) -> type[AttentionBackend]:
33
"""Selects which attention backend to use and lazily imports it."""
@@ -52,6 +53,7 @@ def get_attn_backend(
53
use_mla=use_mla,
54
has_sink=has_sink,
55
use_sparse=use_sparse,
56
+ use_mm_prefix=use_mm_prefix,
57
attn_type=attn_type,
58
)
59
@@ -66,6 +68,7 @@ def _cached_get_attn_backend(
68
use_mla: bool = False,
69
has_sink: bool = False,
70
use_sparse: bool = False,
71
+ use_mm_prefix: bool = False,
72
attn_type: str | None = None,
73
) -> type[AttentionBackend]:
74
from vllm.platforms import current_platform
@@ -87,6 +90,7 @@ def _cached_get_attn_backend(
90
use_mla,
91
has_sink,
92
use_sparse,
93
+ use_mm_prefix,
94
attn_type,
95
)
96
else:
@@ -99,6 +103,7 @@ def _cached_get_attn_backend(
103
use_mla,
104
has_sink,
105
use_sparse,
106
+ use_mm_prefix,
107
attn_type,
108
)
109
if not attention_cls:
vllm/config/model.py
+14
@@ -4,6 +4,7 @@
4
import warnings
5
from collections.abc import Callable
6
from dataclasses import InitVar, field
7
+from functools import cached_property
8
from typing import TYPE_CHECKING, Any, Literal, cast, get_args
9
10
import torch
@@ -1217,6 +1218,19 @@ class ModelConfig:
1218
)
1219
return False
1220
1221
+ @cached_property
1222
+ def is_mm_prefix_lm(self) -> bool:
1223
+ """Whether to use bidirectional attention for mm positions."""
1224
+ MM_PREFIX_LM_MODELS = (
1225
+ "gemma3",
1226
+ # TODO(Isotr0py): Disable paligemma for now before
1227
+ # we supports soft cap attention for FlexAttention
1228
+ # "paligemma",
1229
+ )
1230
+ if not hasattr(self.hf_config, "model_type"):
1231
+ return False
1232
+ return self.hf_config.model_type in MM_PREFIX_LM_MODELS
1233
+
1234
def get_head_size(self) -> int:
1235
# TODO remove hard code
1236
if self.is_deepseek_mla:
vllm/multimodal/inputs.py
+25
@@ -175,6 +175,31 @@ class PlaceholderRange:
175
176
return int(self.is_embed.sum().item())
177
178
+ def extract_embeds_range(self) -> list[tuple[int, int]]:
179
+ """Extract the start and end indices of the embedded region in prompt.
180
+
181
+ For example, given `PlaceholderRange(offset=2, length=5)` and
182
+ `is_embed = [False, True, False, True, True]`, the output is
183
+ `[(1 + offset, 1 + offset), (3 + offset, 4 + offset)]`.
184
+
185
+ Returns:
186
+ A tuple `(start, end)` representing the start and end
187
+ indices (inclusive) of the embedded region.
188
+ Returns full placeholder range if `is_embed` is `None`.
189
+ """
190
+ if self.is_embed is None:
191
+ return [(self.offset, self.offset + self.length)]
192
+
193
+ mask_i = self.is_embed.int()
194
+ starts = torch.nonzero(
195
+ torch.diff(mask_i, prepend=mask_i.new_zeros(1)) == 1
196
+ ).flatten()
197
+ ends = torch.nonzero(
198
+ torch.diff(mask_i, append=mask_i.new_zeros(1)) == -1
199
+ ).flatten()
200
+ ranges = torch.stack((starts, ends), dim=1) + self.offset
201
+ return [tuple(x) for x in ranges.tolist()]
202
+
203
def __eq__(self, other: object) -> bool:
204
if not isinstance(other, self.__class__):
205
return False
vllm/platforms/cpu.py
+1
@@ -133,6 +133,7 @@ class CpuPlatform(Platform):
133
use_mla: bool,
134
has_sink: bool,
135
use_sparse: bool,
136
+ use_mm_prefix: bool,
137
attn_type: str | None = None,
138
) -> str:
139
if selected_backend and selected_backend != AttentionBackendEnum.CPU_ATTN:
vllm/platforms/cuda.py
+19
@@ -233,6 +233,20 @@ class CudaPlatformBase(Platform):
233
"Forcing kv cache block size to 64 for FlashMLASparse backend."
234
)
235
236
+ scheduler_config = vllm_config.scheduler_config
237
+ # Note: model_config may be None during testing
238
+ if (
239
+ model_config is not None
240
+ and model_config.is_mm_prefix_lm
241
+ and scheduler_config.is_multimodal_model
242
+ and not scheduler_config.disable_chunked_mm_input
243
+ ):
244
+ logger.warning(
245
+ "Forcing --disable_chunked_mm_input for models "
246
+ "with multimodal-bidirectional attention."
247
+ )
248
+ scheduler_config.disable_chunked_mm_input = True
249
+
250
@classmethod
251
def get_current_memory_usage(
252
cls, device: torch.types.Device | None = None
@@ -268,6 +282,7 @@ class CudaPlatformBase(Platform):
282
use_mla,
283
has_sink,
284
use_sparse,
285
+ use_mm_prefix,
286
device_capability,
287
attn_type,
288
) -> tuple[
@@ -289,6 +304,7 @@ class CudaPlatformBase(Platform):
304
use_mla,
305
has_sink,
306
use_sparse,
307
+ use_mm_prefix,
308
device_capability,
309
attn_type,
310
)
@@ -312,6 +328,7 @@ class CudaPlatformBase(Platform):
328
use_mla: bool,
329
has_sink: bool,
330
use_sparse: bool,
331
+ use_mm_prefix: bool,
332
attn_type: str | None = None,
333
) -> str:
334
if attn_type is None:
@@ -332,6 +349,7 @@ class CudaPlatformBase(Platform):
349
use_mla,
350
has_sink,
351
use_sparse,
352
+ use_mm_prefix,
353
device_capability,
354
attn_type,
355
)
@@ -356,6 +374,7 @@ class CudaPlatformBase(Platform):
374
use_mla,
375
has_sink,
376
use_sparse,
377
+ use_mm_prefix,
378
device_capability,
379
attn_type,
380
)
vllm/platforms/interface.py
+1
@@ -239,6 +239,7 @@ class Platform:
239
use_mla: bool,
240
has_sink: bool,
241
use_sparse: bool,
242
+ use_mm_prefix: bool,
243
attn_type: str | None = None,
244
) -> str:
245
"""Get the attention backend class of a device."""
vllm/platforms/rocm.py
+1
@@ -216,6 +216,7 @@ class RocmPlatform(Platform):
216
use_mla,
217
has_sink,
218
use_sparse,
219
+ use_mm_prefix,
220
attn_type: str | None = None,
221
) -> str:
222
from vllm._aiter_ops import rocm_aiter_ops
vllm/platforms/tpu.py
+3
-2
@@ -62,8 +62,9 @@ class TpuPlatform(Platform):
62
kv_cache_dtype: str | None,
63
block_size: int,
64
use_mla: bool,
65
- has_sink,
66
- use_sparse,
65
+ has_sink: bool,
66
+ use_sparse: bool,
67
+ use_mm_prefix: bool,
68
attn_type: str | None = None,
69
) -> str:
70
if use_sparse:
vllm/platforms/xpu.py
+2
-1
@@ -48,7 +48,8 @@ class XPUPlatform(Platform):
48
block_size: int,
49
use_mla: bool,
50
has_sink: bool,
51
- use_sparse,
51
+ use_sparse: bool,
52
+ use_mm_prefix: bool,
53
attn_type: str | None = None,
54
) -> str:
55
from vllm.v1.attention.backends.utils import set_kv_cache_layout
vllm/v1/attention/backends/flex_attention.py
+62
@@ -17,6 +17,7 @@ from torch.nn.attention.flex_attention import (
17
and_masks,
18
create_block_mask,
19
flex_attention,
20
+ or_masks,
21
)
22
23
from vllm.attention.backends.abstract import (
@@ -42,6 +43,7 @@ from vllm.v1.kv_cache_interface import AttentionSpec
43
44
logger = init_logger(__name__)
45
46
+torch._dynamo.config.recompile_limit = 16
47
create_block_mask_compiled = torch.compile(
48
create_block_mask, fullgraph=True, mode="reduce-overhead"
49
)
@@ -91,6 +93,11 @@ class FlexAttentionBackend(AttentionBackend):
93
"""FlexAttention supports both decoder and encoder-only attention."""
94
return attn_type in (AttentionType.DECODER, AttentionType.ENCODER_ONLY)
95
96
+ @classmethod
97
+ def supports_mm_prefix(cls) -> bool:
98
+ """FlexAttention supports full attention for image tokens."""
99
+ return True
100
+
101
@staticmethod
102
def get_impl_cls() -> type["FlexAttentionImpl"]:
103
return FlexAttentionImpl
@@ -316,6 +323,7 @@ class FlexAttentionMetadata:
323
kv_block_size: int = 16
324
transformed_score_mod: _score_mod_signature | None = None
325
sliding_window: int | None = None
326
+ mm_prefix_range: dict[int, list[tuple[int, int]]] | None = None
327
328
@cached_property
329
def logical_block_ids(self):
@@ -443,6 +451,45 @@ class FlexAttentionMetadata:
451
452
return final_mask_mod if self.causal else sliding_window_mask_mod
453
454
+ def get_prefix_lm_mask_mod(self) -> _mask_mod_signature:
455
+ """Creates the prefix LM mask_mod function for FlexAttention."""
456
+
457
+ assert self.doc_ids is not None
458
+ request_lookup = self.doc_ids
459
+
460
+ def prefix_lm_mask_mod(
461
+ b: torch.Tensor,
462
+ h: torch.Tensor,
463
+ cu_q_idx: torch.Tensor,
464
+ q_idx: torch.Tensor,
465
+ kv_idx: torch.Tensor,
466
+ ):
467
+ mask = torch.zeros_like(q_idx, dtype=torch.bool)
468
+ for req, doc_range_lst in (self.mm_prefix_range or {}).items():
469
+ req_mask = request_lookup[cu_q_idx] == req
470
+ for start, end in doc_range_lst:
471
+ doc_mask_q = (q_idx >= start) & (q_idx <= end)
472
+ doc_mask_kv = (kv_idx >= start) & (kv_idx <= end)
473
+ mask = mask | (req_mask & doc_mask_q & doc_mask_kv)
474
+ return mask
475
+
476
+ def final_mask_mod(
477
+ b: torch.Tensor,
478
+ h: torch.Tensor,
479
+ q_idx: torch.Tensor,
480
+ physical_kv_idx: torch.Tensor,
481
+ ) -> torch.Tensor:
482
+ (is_valid, logical_q_idx, logical_kv_idx) = (
483
+ self._convert_physical_to_logical(self.doc_ids, q_idx, physical_kv_idx)
484
+ )
485
+ return torch.where(
486
+ is_valid,
487
+ prefix_lm_mask_mod(b, h, q_idx, logical_q_idx, logical_kv_idx),
488
+ False,
489
+ )
490
+
491
+ return final_mask_mod
492
+
493
def get_mask_mod(self):
494
# Stage-1: initialize the base mask_mod
495
# (causal mask for decoder or bidirectional mask for encoder)
@@ -456,6 +503,10 @@ class FlexAttentionMetadata:
503
# Add sliding window mask for sliding window attention
504
sliding_window_mask_mod = self.get_sliding_window_mask_mod()
505
mask_mod = and_masks(mask_mod, sliding_window_mask_mod)
506
+ if self.mm_prefix_range:
507
+ # Add prefix LM mask for vision-language prefix LM attention
508
+ prefix_lm_mask_mod = self.get_prefix_lm_mask_mod()
509
+ mask_mod = or_masks(mask_mod, prefix_lm_mask_mod)
510
return mask_mod
511
512
def get_transformed_score_mod(self) -> _score_mod_signature | None:
@@ -709,6 +760,7 @@ class FlexAttentionImpl(AttentionImpl):
760
sliding_window: int | None
761
alibi_slopes: torch.Tensor | None
762
logits_soft_cap: float | None
763
+ mm_prefix_range: dict[int, list[tuple[int, int]]] | None = None
764
765
def __init__(
766
self,
@@ -810,11 +862,21 @@ class FlexAttentionImpl(AttentionImpl):
862
863
num_actual_tokens = attn_metadata.num_actual_tokens
864
865
+ needs_rebuild_block_mask = False
866
if attn_metadata.sliding_window != self.sliding_window:
867
attn_metadata.sliding_window = self.sliding_window
868
if attn_metadata.direct_build:
869
# update mask mod in attention metadata
870
attn_metadata.mask_mod = attn_metadata.get_mask_mod()
871
+ needs_rebuild_block_mask = True
872
+
873
+ if self.mm_prefix_range != getattr(attn_metadata, "mm_prefix_range", None):
874
+ self.mm_prefix_range = attn_metadata.mm_prefix_range
875
+ attn_metadata.mask_mod = attn_metadata.get_mask_mod()
876
+ needs_rebuild_block_mask = True
877
+
878
+ if needs_rebuild_block_mask:
879
+ if attn_metadata.direct_build and attn_metadata.causal:
880
attn_metadata.block_mask = attn_metadata._build_block_mask_direct()
881
else:
882
attn_metadata.block_mask = attn_metadata.build_block_mask()
vllm/v1/worker/gpu_model_runner.py
+25
-1
@@ -48,7 +48,10 @@ from vllm.distributed.parallel_state import (
48
is_global_first_rank,
49
prepare_communication_buffer_for_model,
50
)
51
-from vllm.forward_context import BatchDescriptor, set_forward_context
51
+from vllm.forward_context import (
52
+ BatchDescriptor,
53
+ set_forward_context,
54
+)
55
from vllm.logger import init_logger
56
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
57
from vllm.model_executor.layers.rotary_embedding import (
@@ -329,6 +332,7 @@ class GPUModelRunner(
332
self.use_alibi = model_config.uses_alibi
333
334
self.cascade_attn_enabled = not self.model_config.disable_cascade_attn
335
+ self.is_mm_prefix_lm = self.model_config.is_mm_prefix_lm
336
337
# Multi-modal data support
338
self.mm_registry = MULTIMODAL_REGISTRY
@@ -1700,6 +1704,26 @@ class GPUModelRunner(
1704
for layer_name in attn_group.layer_names:
1705
attn_metadata[layer_name] = attn_metadata_i
1706
1707
+ if self.is_mm_prefix_lm:
1708
+ req_doc_ranges = {}
1709
+ for req_id in self.input_batch.req_ids:
1710
+ image_doc_ranges = []
1711
+ req_state = self.requests[req_id]
1712
+ for mm_feature in req_state.mm_features:
1713
+ pos_info = mm_feature.mm_position
1714
+ img_doc_range = pos_info.extract_embeds_range()
1715
+ image_doc_ranges.extend(img_doc_range)
1716
+ req_idx = self.input_batch.req_id_to_index[req_id]
1717
+ req_doc_ranges[req_idx] = image_doc_ranges
1718
+
1719
+ if isinstance(attn_metadata, list):
1720
+ for ub_metadata in attn_metadata:
1721
+ for _metadata in ub_metadata.values():
1722
+ _metadata.mm_prefix_range = req_doc_ranges # type: ignore[attr-defined]
1723
+ else:
1724
+ for _metadata in attn_metadata.values():
1725
+ _metadata.mm_prefix_range = req_doc_ranges # type: ignore[attr-defined]
1726
+
1727
if spec_decode_common_attn_metadata is not None and (
1728
num_reqs != num_reqs_padded or num_tokens != num_tokens_padded
1729
):