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 15, 18:30 UTC
[Bugfix] Re-enable Gemma3 for V1 (#14980)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Mar 19, 2025 at 14:58 UTC
61f412187d972a006aef1653bfe348aeaefb6a0b
8 files changed
+419
-175
docs/source/models/supported_models.md
+2
-5
@@ -768,7 +768,7 @@ See [this page](#generative-models) for more information on how to use generativ
768
* `google/gemma-3-4b-it`, `google/gemma-3-27b-it`, etc.
769
* ✅︎
770
* ✅︎
771
- *
771
+ * ⚠️
772
- * `GLM4VForCausalLM`<sup>^</sup>
773
* GLM-4V
774
* T + I
@@ -951,13 +951,10 @@ V0 correctly implements the model's attention pattern:
951
952
V1 currently uses a simplified attention pattern:
953
- Uses causal attention for all tokens, including image tokens
954
-- Generates reasonable outputs but does not match the original model's attention for text + image inputs
954
+- Generates reasonable outputs but does not match the original model's attention for text + image inputs, especially when `{"do_pan_and_scan": True}`
955
- Will be updated in the future to support the correct behavior
956
-- Does not support `"do_pan_and_scan": True`
956
957
This limitation exists because the model's mixed attention pattern (bidirectional for images, causal otherwise) is not yet supported by vLLM's attention backends.
959
-
960
-For these reasons, `Gemma3ForConditionalGeneration` is supported only on V0 at the moment.
958
:::
959
960
:::{note}
tests/multimodal/test_processing.py
+54
-1
@@ -19,7 +19,8 @@ from vllm.multimodal.processing import (PlaceholderFeaturesInfo,
19
apply_token_matches,
20
find_mm_placeholders,
21
find_text_matches, find_token_matches,
22
- iter_token_matches)
22
+ iter_token_matches,
23
+ replace_token_matches)
24
# yapf: enable
25
from vllm.multimodal.profiling import MultiModalProfiler
26
from vllm.transformers_utils.tokenizer import (AnyTokenizer,
@@ -89,6 +90,58 @@ def test_iter_token_matches(token_ids, match_ids, expected):
90
assert all(match_len == len(match_ids) for match_len in match_lens)
91
92
93
+# yapf: disable
94
+@pytest.mark.parametrize(
95
+ ("token_ids", "match_ids", "new_ids", "expected"),
96
+ [
97
+ ([], [], [-1], []),
98
+ ([], [32000], [-1], []),
99
+ (
100
+ [32000, 32000, 32000],
101
+ [32000],
102
+ [-1],
103
+ [-1, -1, -1],
104
+ ),
105
+ (
106
+ [32000, 32000, 32000],
107
+ [32000, 32000],
108
+ [-1],
109
+ [-1, 32000],
110
+ ),
111
+ (
112
+ [32000, 32000, 32000],
113
+ [32000, 32000, 32000],
114
+ [-1],
115
+ [-1],
116
+ ),
117
+ (
118
+ [9833, 28747, 32000, 32000, 32000, 9833, 28747, 32000, 32000, 918],
119
+ [28747, 32000],
120
+ [-1],
121
+ [9833, -1, 32000, 32000, 9833, -1, 32000, 918],
122
+ ),
123
+ (
124
+ [9833, 28747, 32000, 32000, 32000, 9833, 28747, 32000, 32000, 918],
125
+ [28747, 32000, 32000, 32000],
126
+ [-1],
127
+ [9833, -1, 9833, 28747, 32000, 32000, 918],
128
+ ),
129
+ (
130
+ [9833, 28747, 32000, 32000, 32000, 9833, 28747, 32000, 32000, 918],
131
+ [28747, 0, 32000],
132
+ [-1],
133
+ [9833, 28747, 32000, 32000, 32000, 9833, 28747, 32000, 32000, 918],
134
+ ),
135
+ ],
136
+)
137
+# yapf: enable
138
+def test_replace_token_matches(token_ids, match_ids, new_ids, expected):
139
+ result = replace_token_matches(token_ids, match_ids, new_ids)
140
+
141
+ # Manually constructed results
142
+ assert result == expected
143
+
144
+
145
# yapf: disable
146
@pytest.mark.parametrize(
147
("prompt", "target_by_key", "expected_by_key"),
vllm/model_executor/models/gemma3_mm.py
+211
-36
@@ -1,34 +1,43 @@
1
# SPDX-License-Identifier: Apache-2.0
2
import math
3
-from typing import (Any, Iterable, Literal, Mapping, Optional, Sequence, Set,
4
- Tuple, TypedDict, Union)
3
+from collections.abc import Iterable, Mapping, Sequence
4
+from typing import Any, Literal, Optional, Set, Tuple, TypedDict, Union
5
6
import torch
7
from torch import nn
8
from transformers import BatchFeature, Gemma3Config, Gemma3Processor
9
from transformers.models.gemma3.processing_gemma3 import Gemma3ProcessorKwargs
10
11
+import vllm.envs as envs
12
from vllm.config import VllmConfig
13
from vllm.logger import init_logger
14
from vllm.model_executor.layers.layernorm import GemmaRMSNorm
15
from vllm.model_executor.layers.sampler import SamplerOutput
16
from vllm.model_executor.models.module_mapping import MultiModelKeys
17
from vllm.model_executor.sampling_metadata import SamplingMetadata
17
-from vllm.multimodal import MULTIMODAL_REGISTRY
18
-from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargs
18
+from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalKwargs
19
+from vllm.multimodal.inputs import MultiModalFieldConfig
20
from vllm.multimodal.parse import (ImageProcessorItems, ImageSize,
21
MultiModalDataItems)
22
+# yapf: disable
23
from vllm.multimodal.processing import (BaseMultiModalProcessor,
22
- BaseProcessingInfo, PromptReplacement,
23
- PromptUpdate, encode_tokens)
24
+ BaseProcessingInfo, BoundPromptUpdate,
25
+ PlaceholderFeaturesInfo,
26
+ PromptReplacement, PromptTargetMatch,
27
+ PromptUpdate, PromptUpdateDetails,
28
+ encode_tokens, find_mm_placeholders,
29
+ replace_token_matches)
30
+# yapf: enable
31
from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
32
from vllm.sequence import IntermediateTensors
33
+from vllm.utils import flatten_2d_lists
34
35
from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
28
- SupportsMultiModal, SupportsPP, SupportsV0Only)
36
+ SupportsMultiModal, SupportsPP)
37
from .siglip import SiglipVisionModel
38
from .utils import (AutoWeightsLoader, flatten_bn, init_vllm_registered_model,
39
maybe_prefix, merge_multimodal_embeddings)
40
+from .vision import scatter_patch_features, select_patch_features
41
42
logger = init_logger(__name__)
43
@@ -37,13 +46,25 @@ class Gemma3ImagePixelInputs(TypedDict):
46
type: Literal["pixel_values"]
47
pixel_values: torch.Tensor
48
"""
40
- Shape: `(num_crops_total, num_channels, height, width)`
49
+ Shape: `(num_patches_total, num_channels, height, width)`
50
42
- `num_crops_total` is the total number of crops
51
+ `num_patches_total` is the total number of patches
52
over each image over each prompt in the batch.
53
"""
45
- num_crops: torch.Tensor
46
- """Shape: `(batch_size * num_images,)`"""
54
+
55
+ num_patches: torch.Tensor
56
+ """Shape: `(batch_size * num_images)`"""
57
+
58
+ embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
59
+ """
60
+ A boolean mask indicating which image embeddings correspond
61
+ to patch tokens.
62
+
63
+ Shape: `(batch_size, num_images, num_embeds)`
64
+ """
65
+
66
+ num_embeds: Union[torch.Tensor, list[torch.Tensor]]
67
+ """Shape: `(batch_size, num_images)`"""
68
69
70
Gemma3ImageInputs = Gemma3ImagePixelInputs
@@ -51,6 +72,9 @@ Gemma3ImageInputs = Gemma3ImagePixelInputs
72
73
class Gemma3ProcessingInfo(BaseProcessingInfo):
74
75
+ def get_hf_config(self):
76
+ return self.ctx.get_hf_config(Gemma3Config)
77
+
78
def get_hf_processor(self, **kwargs: object):
79
return self.ctx.get_hf_processor(Gemma3Processor, **kwargs)
80
@@ -114,6 +138,11 @@ class Gemma3ProcessingInfo(BaseProcessingInfo):
138
if not do_pan_and_scan:
139
return 0
140
141
+ if envs.VLLM_USE_V1:
142
+ logger.warning_once(
143
+ "`do_pan_and_scan=True` has suboptimal results on V1 "
144
+ "because of the simplified attention pattern being used.")
145
+
146
# Based on Gemma3ImageProcessor.pan_and_scan
147
if image_width >= image_height:
148
if image_width / image_height < pan_and_scan_min_ratio_to_activate:
@@ -154,7 +183,7 @@ class Gemma3ProcessingInfo(BaseProcessingInfo):
183
image_width: int,
184
image_height: int,
185
processor: Optional[Gemma3Processor],
157
- ) -> str:
186
+ ) -> PromptUpdateDetails:
187
if processor is None:
188
processor = self.get_hf_processor()
189
@@ -175,7 +204,11 @@ class Gemma3ProcessingInfo(BaseProcessingInfo):
204
f"Here is the original image {image_token} and here are some "
205
f"crops to help you see better {crops_image_tokens}")
206
178
- return image_text.replace(image_token, processor.full_image_sequence)
207
+ repl_full = image_text.replace(image_token,
208
+ processor.full_image_sequence)
209
+ repl_features = repl_full.strip("\n")
210
+
211
+ return PromptUpdateDetails(full=repl_full, features=repl_features)
212
213
def get_num_image_tokens(
214
self,
@@ -193,7 +226,7 @@ class Gemma3ProcessingInfo(BaseProcessingInfo):
226
227
image_repl_tokens = encode_tokens(
228
tokenizer,
196
- image_repl,
229
+ image_repl.features,
230
add_special_tokens=False,
231
)
232
return len(image_repl_tokens)
@@ -240,12 +273,8 @@ class Gemma3DummyInputsBuilder(BaseDummyInputsBuilder[Gemma3ProcessingInfo]):
273
num_images=num_images)
274
}
275
243
- # NOTE: We need to separate the image tokens here because
244
- # encode("\n\n\n\n") != encode("\n\n") * 2, which interferes
245
- # with the detection of prompt updates when the image tokens are
246
- # right next to each other
276
return ProcessorInputs(
248
- prompt_text=" ".join([image_token] * num_images),
277
+ prompt_text=image_token * num_images,
278
mm_data=mm_data,
279
)
280
@@ -278,13 +307,39 @@ class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
307
]
308
hf_processor = self.info.get_hf_processor(**mm_kwargs)
309
310
+ image_repl_features = [
311
+ self.info.get_image_repl(image_width=size.width,
312
+ image_height=size.height,
313
+ processor=hf_processor).features
314
+ for size in image_sizes
315
+ ]
316
+
317
+ tokenizer = self.info.get_tokenizer()
318
+ image_repls_feature_tokens = [
319
+ tokenizer.encode(image_repl, add_special_tokens=False)
320
+ for image_repl in image_repl_features
321
+ ]
322
+ num_embeds = [
323
+ len(image_repl_feature_tokens)
324
+ for image_repl_feature_tokens in image_repls_feature_tokens
325
+ ]
326
+ processed_outputs["num_embeds"] = torch.tensor(num_embeds)
327
+
328
+ vocab = tokenizer.get_vocab()
329
+ image_token_id = vocab[tokenizer.image_token]
330
+
331
+ embed_is_patch = [
332
+ torch.tensor(image_repl_tokens) == image_token_id
333
+ for image_repl_tokens in image_repls_feature_tokens
334
+ ]
335
+ processed_outputs["embed_is_patch"] = embed_is_patch
336
+
337
num_crops = [
338
self.info.get_num_crops(image_width=size.width,
339
image_height=size.height,
340
processor=hf_processor)
341
for size in image_sizes
342
]
287
-
343
processed_outputs["num_crops"] = torch.tensor(num_crops)
344
345
return processed_outputs
@@ -300,6 +355,8 @@ class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
355
pixel_values=MultiModalFieldConfig.flat_from_sizes(
356
"image", num_crops + 1),
357
num_crops=MultiModalFieldConfig.batched("image"),
358
+ embed_is_patch=MultiModalFieldConfig.batched("image"),
359
+ num_embeds=MultiModalFieldConfig.batched("image"),
360
)
361
362
def _get_prompt_updates(
@@ -329,6 +386,91 @@ class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
386
)
387
]
388
389
+ def _apply_token_matches(
390
+ self,
391
+ prompt: list[int],
392
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
393
+ mm_item_counts: Mapping[str, int],
394
+ ) -> list[int]:
395
+ token_ids = super()._apply_token_matches(
396
+ prompt,
397
+ mm_matches,
398
+ mm_item_counts,
399
+ )
400
+
401
+ # "\n\n\n" and "\n\n\n\n" are single tokens
402
+ # Since our replacement can insert "\n\n" next to "\n"
403
+ # tokens, we have to combine them to be consistent with
404
+ # the output of the tokenizer
405
+ tokenizer = self.info.get_tokenizer()
406
+ vocab = tokenizer.get_vocab()
407
+ newline_1 = vocab["\n"]
408
+ newline_2 = vocab["\n\n"]
409
+ newline_3 = vocab["\n\n\n"]
410
+ newline_4 = vocab["\n\n\n\n"]
411
+
412
+ token_ids = replace_token_matches(
413
+ token_ids,
414
+ [newline_1, newline_2],
415
+ [newline_3],
416
+ )
417
+ token_ids = replace_token_matches(
418
+ token_ids,
419
+ [newline_2, newline_1],
420
+ [newline_3],
421
+ )
422
+ token_ids = replace_token_matches(
423
+ token_ids,
424
+ [newline_2, newline_2],
425
+ [newline_4],
426
+ )
427
+
428
+ return token_ids
429
+
430
+ def _find_mm_placeholders(
431
+ self,
432
+ mm_prompt_updates: Mapping[str, Sequence[BoundPromptUpdate]],
433
+ new_token_ids: list[int],
434
+ mm_item_counts: Mapping[str, int],
435
+ ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
436
+ # We need to detect "\n\n" inside "\n\n\n" and "\n\n\n\n"
437
+ tokenizer = self.info.get_tokenizer()
438
+ vocab = tokenizer.get_vocab()
439
+ newline_1 = vocab["\n"]
440
+ newline_2 = vocab["\n\n"]
441
+ newline_3 = vocab["\n\n\n"]
442
+ newline_4 = vocab["\n\n\n\n"]
443
+
444
+ def get_repl_toks(tok: int) -> list[int]:
445
+ if tok == newline_3:
446
+ return [newline_1, newline_2]
447
+ if tok == newline_4:
448
+ return [newline_2, newline_2]
449
+
450
+ return [tok]
451
+
452
+ repl_token_ids = list[int]()
453
+ repl_orig_idxs = list[int]()
454
+ for orig_idx, orig_tok in enumerate(new_token_ids):
455
+ repl_toks = get_repl_toks(orig_tok)
456
+ repl_token_ids.extend(repl_toks)
457
+ repl_orig_idxs.extend(orig_idx for _ in range(len(repl_toks)))
458
+
459
+ repls = find_mm_placeholders(mm_prompt_updates, repl_token_ids,
460
+ mm_item_counts)
461
+
462
+ return {
463
+ modality: [
464
+ PlaceholderFeaturesInfo(
465
+ modality=p.modality,
466
+ item_idx=p.item_idx,
467
+ start_idx=repl_orig_idxs[p.start_idx],
468
+ tokens=p.tokens,
469
+ ) for p in placeholders
470
+ ]
471
+ for modality, placeholders in repls.items()
472
+ }
473
+
474
475
class Gemma3MultiModalProjector(nn.Module):
476
@@ -374,7 +516,7 @@ class Gemma3MultiModalProjector(nn.Module):
516
info=Gemma3ProcessingInfo,
517
dummy_inputs=Gemma3DummyInputsBuilder)
518
class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
377
- SupportsLoRA, SupportsV0Only):
519
+ SupportsLoRA):
520
packed_modules_mapping = {
521
"qkv_proj": [
522
"q_proj",
@@ -415,6 +557,10 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
557
self.make_empty_intermediate_tensors = (
558
self.language_model.make_empty_intermediate_tensors)
559
560
+ @property
561
+ def dtype(self):
562
+ return next(self.parameters()).dtype
563
+
564
@property
565
def sampler(self):
566
return self.language_model.sampler
@@ -438,6 +584,8 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
584
self, **kwargs: object) -> Optional[Gemma3ImageInputs]:
585
pixel_values = kwargs.pop("pixel_values", None)
586
num_crops = kwargs.pop("num_crops", None)
587
+ embed_is_patch = kwargs.pop("embed_is_patch", None)
588
+ num_embeds = kwargs.pop("num_embeds", None)
589
image_embeds = kwargs.pop("image_embeds", None)
590
assert image_embeds is None, "Gemma3 does not support image_embeds."
591
if pixel_values is None:
@@ -448,16 +596,26 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
596
f"Got type: {type(pixel_values)}")
597
598
if not isinstance(num_crops, (torch.Tensor, list)):
451
- raise ValueError("Incorrect type of num_crops values. "
599
+ raise ValueError("Incorrect type of num_crops. "
600
f"Got type: {type(num_crops)}")
601
602
+ if not isinstance(embed_is_patch, (torch.Tensor, list)):
603
+ raise ValueError("Incorrect type of embed_is_patch. "
604
+ f"Got type: {type(embed_is_patch)}")
605
+
606
+ if not isinstance(num_embeds, (torch.Tensor, list)):
607
+ raise ValueError("Incorrect type of num_embeds. "
608
+ f"Got type: {type(num_embeds)}")
609
+
610
pixel_values = flatten_bn(pixel_values, concat=True)
611
num_crops = flatten_bn(num_crops, concat=True)
612
613
return Gemma3ImagePixelInputs(
614
type="pixel_values",
615
pixel_values=self._validate_pixel_values(pixel_values),
460
- num_crops=num_crops,
616
+ num_patches=num_crops + 1,
617
+ embed_is_patch=embed_is_patch,
618
+ num_embeds=num_embeds,
619
)
620
621
def _image_pixels_to_features(
@@ -472,36 +630,51 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
630
def _process_image_input(
631
self,
632
image_input: Gemma3ImageInputs,
475
- ) -> torch.Tensor:
633
+ ) -> tuple[torch.Tensor, ...]:
634
assert self.vision_tower is not None
635
636
pixel_values = image_input["pixel_values"]
479
- vision_outputs = self._image_pixels_to_features(
637
+ num_patches = image_input["num_patches"]
638
+
639
+ image_features = self._image_pixels_to_features(
640
self.vision_tower,
641
pixel_values,
642
)
483
- return self.multi_modal_projector(vision_outputs)
643
+ image_embeds = self.multi_modal_projector(image_features)
644
+
645
+ return image_embeds.split(num_patches.tolist())
646
647
def get_multimodal_embeddings(
648
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
649
image_input = self._parse_and_validate_image_input(**kwargs)
650
if image_input is None:
651
return None
490
- vision_embeddings = self._process_image_input(image_input)
491
- return vision_embeddings
652
+
653
+ image_features = self._process_image_input(image_input)
654
+
655
+ if kwargs.get("v0_path", False):
656
+ return image_features
657
+
658
+ return flatten_2d_lists(
659
+ scatter_patch_features(*args) for args in zip(
660
+ image_features,
661
+ image_input["num_embeds"],
662
+ image_input["embed_is_patch"],
663
+ ))
664
665
def get_input_embeddings(
666
self,
667
input_ids: torch.Tensor,
668
multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
669
) -> torch.Tensor:
498
- if multimodal_embeddings is None:
499
- inputs_embeds = self.language_model.get_input_embeddings(input_ids)
500
- else:
501
- inputs_embeds = self.language_model.get_input_embeddings(input_ids)
670
+ inputs_embeds = self.language_model.get_input_embeddings(input_ids)
671
+ if multimodal_embeddings is not None:
672
inputs_embeds = merge_multimodal_embeddings(
503
- input_ids, inputs_embeds, multimodal_embeddings,
504
- self.config.image_token_index)
673
+ input_ids,
674
+ inputs_embeds,
675
+ select_patch_features(multimodal_embeddings),
676
+ self.config.image_token_index,
677
+ )
678
return inputs_embeds
679
680
def forward(self,
@@ -516,6 +689,7 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
689
# NOTE: In v1, inputs_embeds is always generated at model runner, this
690
# condition is for v0 compatibility.
691
elif inputs_embeds is None:
692
+ kwargs.update({"v0_path": True})
693
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
694
695
inputs_embeds = self.get_input_embeddings(input_ids,
@@ -524,8 +698,9 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
698
kwargs = self.prepare_attn_masks(
699
input_ids,
700
positions,
527
- mask_dtype=vision_embeddings.dtype,
528
- **kwargs)
701
+ mask_dtype=self.dtype,
702
+ **kwargs,
703
+ )
704
input_ids = None
705
706
hidden_states = self.language_model.model(input_ids,
vllm/model_executor/models/llava.py
+18
-51
@@ -18,7 +18,7 @@ from transformers.models.pixtral import PixtralProcessor
18
19
from vllm.config import VllmConfig
20
from vllm.inputs import InputProcessingContext
21
-from vllm.jsontree import JSONTree, json_map_leaves
21
+from vllm.jsontree import json_map_leaves
22
from vllm.model_executor.layers.activation import get_act_fn
23
from vllm.model_executor.layers.linear import (ColumnParallelLinear,
24
RowParallelLinear)
@@ -27,8 +27,7 @@ from vllm.model_executor.layers.sampler import SamplerOutput, get_sampler
27
from vllm.model_executor.sampling_metadata import SamplingMetadata
28
from vllm.multimodal import MULTIMODAL_REGISTRY
29
from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
30
- MultiModalInputs, MultiModalKwargs,
31
- NestedTensors)
30
+ MultiModalInputs, MultiModalKwargs)
31
from vllm.multimodal.parse import (ImageEmbeddingItems, ImageProcessorItems,
32
ImageSize, MultiModalDataItems)
33
from vllm.multimodal.processing import (BaseMultiModalProcessor,
@@ -44,7 +43,8 @@ from .pixtral import PixtralHFEncoderInfo, PixtralHFVisionModel
43
from .siglip import SiglipVisionModel
44
from .utils import (AutoWeightsLoader, flatten_bn, init_vllm_registered_model,
45
maybe_prefix, merge_multimodal_embeddings)
47
-from .vision import get_vision_encoder_info
46
+from .vision import (get_vision_encoder_info, scatter_patch_features,
47
+ select_patch_features)
48
49
50
class LlavaImagePixelInputs(TypedDict):
@@ -76,7 +76,7 @@ class PixtralHFImagePixelInputs(TypedDict):
76
Shape: `(batch_size, num_images, num_embeds)`
77
"""
78
79
- num_patches: Union[torch.Tensor, list[torch.Tensor]]
79
+ num_embeds: Union[torch.Tensor, list[torch.Tensor]]
80
"""Shape: `(batch_size, num_images)`"""
81
82
@@ -352,15 +352,15 @@ class PixtralHFMultiModalProcessor(
352
image_height=pixel_value.shape[-2],
353
) for pixel_value in processed_outputs["pixel_values"]
354
]
355
- num_patches = torch.tensor([(ncols + 1) * nrows
356
- for ncols, nrows in tile_sizes])
355
+ num_embeds = torch.tensor([(ncols + 1) * nrows
356
+ for ncols, nrows in tile_sizes])
357
# Each image may result to masks of different sizes, so we need to
358
- # later use `num_patches` to get per-image masks.
358
+ # later use `num_embeds` to get per-image masks.
359
embed_is_patch = [
360
torch.tensor(([True] * ncols + [False]) * nrows)
361
for ncols, nrows in tile_sizes
362
]
363
- processed_outputs["num_patches"] = num_patches
363
+ processed_outputs["num_embeds"] = num_embeds
364
processed_outputs["embed_is_patch"] = embed_is_patch
365
366
return processed_outputs
@@ -372,7 +372,7 @@ class PixtralHFMultiModalProcessor(
372
) -> Mapping[str, MultiModalFieldConfig]:
373
return dict(
374
pixel_values=MultiModalFieldConfig.batched("image"),
375
- num_patches=MultiModalFieldConfig.batched("image"),
375
+ num_embeds=MultiModalFieldConfig.batched("image"),
376
embed_is_patch=MultiModalFieldConfig.batched("image"),
377
image_embeds=MultiModalFieldConfig.batched("image"),
378
)
@@ -621,16 +621,16 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
621
raise ValueError("Incorrect type of embed_is_patch. "
622
f"Got type: {type(embed_is_patch)}")
623
624
- num_patches = kwargs.pop("num_patches")
625
- if not isinstance(num_patches, (torch.Tensor, list)):
626
- raise ValueError("Incorrect type of num_patches. "
627
- f"Got type: {type(num_patches)}")
624
+ num_embeds = kwargs.pop("num_embeds")
625
+ if not isinstance(num_embeds, (torch.Tensor, list)):
626
+ raise ValueError("Incorrect type of num_embeds. "
627
+ f"Got type: {type(num_embeds)}")
628
629
return PixtralHFImagePixelInputs(
630
type="pixel_values_pixtral",
631
pixel_values=flatten_bn(pixel_values),
632
embed_is_patch=embed_is_patch,
633
- num_patches=num_patches,
633
+ num_embeds=num_embeds,
634
)
635
636
return LlavaImagePixelInputs(
@@ -716,33 +716,6 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
716
image_embeds = torch.split(image_embeds, feature_sizes)
717
return image_embeds
718
719
- def _get_mm_embeds(
720
- self,
721
- features: torch.Tensor, # Shape: (num_patch, d)
722
- num_patches: torch.Tensor, # Shape: (num_images,)
723
- embed_is_patch: torch.Tensor, # Shape: (num_images, num_embeds)
724
- ) -> tuple[torch.Tensor, ...]:
725
- """Scatter the patch features into a contiguous tensor that corresponds
726
- to the embedding tokens defined by the multimodal processor.
727
-
728
- Mostly copied from `Molmo._get_mm_embeds`. See following fixme comment.
729
- """
730
- # Insert columns of nan values according to `embed_is_patch`. This work
731
- # ideally should be done in `_process_image_input`, but
732
- # `_process_image_input` is used in both V0 and V1 path. It's safer to
733
- # put the logic here.
734
- # FIXME: Move this logic to `_process_image_input` when v0 is
735
- # deprecated. Merge this function with `Molmo._get_mm_embeds`.
736
- num_patches_per_image: list[int] = num_patches.tolist()
737
-
738
- embeds_flat = features.new_full(
739
- (sum(num_patches_per_image), *features.shape[1:]),
740
- fill_value=torch.nan,
741
- )
742
- embeds_flat[embed_is_patch.view(-1)] = features
743
-
744
- return embeds_flat.split(num_patches_per_image)
745
-
719
def get_multimodal_embeddings(
720
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
721
image_input = self._parse_and_validate_image_input(**kwargs)
@@ -757,9 +730,9 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
730
return vision_embeddings
731
732
return flatten_2d_lists(
760
- self._get_mm_embeds(*args) for args in zip(
733
+ scatter_patch_features(*args) for args in zip(
734
vision_embeddings,
762
- image_input["num_patches"],
735
+ image_input["num_embeds"],
736
image_input["embed_is_patch"],
737
))
738
@@ -770,16 +743,10 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
743
) -> torch.Tensor:
744
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
745
if multimodal_embeddings is not None:
773
- # Extract the patch tokens
774
- patch_embeddings = json_map_leaves(
775
- lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
776
- cast(JSONTree[torch.Tensor], multimodal_embeddings),
777
- )
778
-
746
inputs_embeds = merge_multimodal_embeddings(
747
input_ids,
748
inputs_embeds,
782
- cast(NestedTensors, patch_embeddings),
749
+ select_patch_features(multimodal_embeddings),
750
self.config.image_token_index,
751
)
752
return inputs_embeds
vllm/model_executor/models/molmo.py
+4
-11
@@ -4,7 +4,7 @@ import math
4
from collections.abc import Iterable, Mapping, Sequence
5
from dataclasses import dataclass
6
from functools import cached_property, partial
7
-from typing import List, Optional, Set, Tuple, TypedDict, Union, cast
7
+from typing import List, Optional, Set, Tuple, TypedDict, Union
8
9
import numpy as np
10
import torch
@@ -24,7 +24,6 @@ from vllm.distributed import (get_pp_group, get_tensor_model_parallel_rank,
24
get_tensor_model_parallel_world_size,
25
split_tensor_along_last_dim,
26
tensor_model_parallel_all_gather)
27
-from vllm.jsontree import JSONTree, json_map_leaves
27
from vllm.model_executor import SamplingMetadata
28
from vllm.model_executor.layers.activation import (MulAndSilu, QuickGELU,
29
SiluAndMul)
@@ -42,8 +41,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
41
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
42
from vllm.model_executor.models.module_mapping import MultiModelKeys
43
from vllm.multimodal import MULTIMODAL_REGISTRY
45
-from vllm.multimodal.inputs import (MultiModalFieldConfig, MultiModalKwargs,
46
- NestedTensors)
44
+from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargs
45
from vllm.multimodal.parse import (ImageProcessorItems, ImageSize,
46
MultiModalDataItems)
47
from vllm.multimodal.processing import (BaseMultiModalProcessor,
@@ -59,6 +57,7 @@ from .utils import (AutoWeightsLoader, WeightsMapper, flatten_bn,
57
is_pp_missing_parameter,
58
make_empty_intermediate_tensors_factory, make_layers,
59
maybe_prefix, merge_multimodal_embeddings)
60
+from .vision import select_patch_features
61
62
# TODO: hard-coded for now. Consider making it configurable.
63
VIT_LAYERS = [-2, -9]
@@ -1602,16 +1601,10 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1601
if multimodal_embeddings is not None:
1602
assert self.img_patch_id is not None
1603
1605
- # Extract the patch tokens scattered in _get_mm_embeds
1606
- patch_embeddings = json_map_leaves(
1607
- lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
1608
- cast(JSONTree[torch.Tensor], multimodal_embeddings),
1609
- )
1610
-
1604
inputs_embeds = merge_multimodal_embeddings(
1605
input_ids,
1606
inputs_embeds,
1614
- cast(NestedTensors, patch_embeddings),
1607
+ select_patch_features(multimodal_embeddings),
1608
self.img_patch_id,
1609
)
1610
return inputs_embeds
vllm/model_executor/models/pixtral.py
+16
-48
@@ -4,7 +4,7 @@ import math
4
from collections.abc import Iterable, Mapping, Sequence
5
from dataclasses import dataclass, fields
6
from functools import cached_property
7
-from typing import List, Literal, Optional, Set, Tuple, TypedDict, Union, cast
7
+from typing import List, Literal, Optional, Set, Tuple, TypedDict, Union
8
9
import torch
10
import torch.nn as nn
@@ -22,7 +22,6 @@ from transformers.tokenization_utils_base import TextInput
22
23
from vllm.config import VllmConfig
24
from vllm.distributed import divide, get_tensor_model_parallel_world_size
25
-from vllm.jsontree import JSONTree, json_map_leaves
25
from vllm.model_executor.layers.activation import get_act_and_mul_fn
26
from vllm.model_executor.layers.layernorm import RMSNorm
27
from vllm.model_executor.layers.linear import (MergedColumnParallelLinear,
@@ -48,7 +47,8 @@ from vllm.utils import flatten_2d_lists
47
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
48
from .utils import (flatten_bn, init_vllm_registered_model, maybe_prefix,
49
merge_multimodal_embeddings)
51
-from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
50
+from .vision import (VisionEncoderInfo, resolve_visual_encoder_outputs,
51
+ scatter_patch_features, select_patch_features)
52
53
try:
54
from xformers import ops as xops
@@ -77,7 +77,7 @@ class PixtralImagePixelInputs(TypedDict):
77
Shape: `(batch_size, num_images, num_embeds)`
78
"""
79
80
- num_patches: Union[torch.Tensor, list[torch.Tensor]]
80
+ num_embeds: Union[torch.Tensor, list[torch.Tensor]]
81
"""Shape: `(batch_size, num_images)`"""
82
83
@@ -153,7 +153,7 @@ class PixtralProcessorAdapter:
153
images_processed = list[torch.Tensor]()
154
images_tokens = list[torch.Tensor]()
155
images_embed_is_patch = list[torch.Tensor]()
156
- images_num_patches = list[int]()
156
+ images_num_embeds = list[int]()
157
158
for image in images:
159
image_inputs = self.image_processor(ImageChunk(image=image))
@@ -163,13 +163,13 @@ class PixtralProcessorAdapter:
163
images_processed.append(image_processed)
164
images_tokens.append(image_tokens)
165
images_embed_is_patch.append(image_tokens == image_token_id)
166
- images_num_patches.append(len(image_tokens))
166
+ images_num_embeds.append(len(image_tokens))
167
168
return {
169
"input_ids": torch.cat(images_tokens)[None].expand(len(text), -1),
170
"images": images_processed,
171
"embed_is_patch": images_embed_is_patch,
172
- "num_patches": torch.tensor(images_num_patches),
172
+ "num_embeds": torch.tensor(images_num_embeds),
173
}
174
175
@@ -273,7 +273,7 @@ class PixtralMultiModalProcessor(BaseMultiModalProcessor[PixtralProcessingInfo]
273
return dict(
274
images=MultiModalFieldConfig.batched("image"),
275
embed_is_patch=MultiModalFieldConfig.batched("image"),
276
- num_patches=MultiModalFieldConfig.batched("image"),
276
+ num_embeds=MultiModalFieldConfig.batched("image"),
277
)
278
279
def _get_prompt_updates(
@@ -394,16 +394,16 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
394
raise ValueError("Incorrect type of embed_is_patch. "
395
f"Got type: {type(embed_is_patch)}")
396
397
- num_patches = kwargs.pop("num_patches")
398
- if not isinstance(num_patches, (torch.Tensor, list)):
399
- raise ValueError("Incorrect type of num_patches. "
400
- f"Got type: {type(num_patches)}")
397
+ num_embeds = kwargs.pop("num_embeds")
398
+ if not isinstance(num_embeds, (torch.Tensor, list)):
399
+ raise ValueError("Incorrect type of num_embeds. "
400
+ f"Got type: {type(num_embeds)}")
401
402
return PixtralImagePixelInputs(
403
type="pixel_values",
404
images=flatten_bn(images),
405
embed_is_patch=embed_is_patch,
406
- num_patches=num_patches,
406
+ num_embeds=num_embeds,
407
)
408
409
def _process_image_input(
@@ -433,33 +433,6 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
433
image_embeds = torch.split(image_embeds, feature_sizes)
434
return image_embeds
435
436
- def _get_mm_embeds(
437
- self,
438
- features: torch.Tensor, # Shape: (num_patch, d)
439
- num_patches: torch.Tensor, # Shape: (num_images,)
440
- embed_is_patch: torch.Tensor, # Shape: (num_images, num_embeds)
441
- ) -> tuple[torch.Tensor, ...]:
442
- """Scatter the patch features into a contiguous tensor that corresponds
443
- to the embedding tokens defined by the multimodal processor.
444
-
445
- Mostly copied from `Molmo._get_mm_embeds`. See following fixme comment.
446
- """
447
- # Insert columns of nan values according to `embed_is_patch`. This work
448
- # ideally should be done in `_process_image_input`, but
449
- # `_process_image_input` is used in both V0 and V1 path. It's safer to
450
- # put the logic here.
451
- # FIXME: Move this logic to `_process_image_input` when v0 is
452
- # deprecated. Merge this function with `Molmo._get_mm_embeds`.
453
- num_patches_per_image: list[int] = num_patches.tolist()
454
-
455
- embeds_flat = features.new_full(
456
- (sum(num_patches_per_image), *features.shape[1:]),
457
- fill_value=torch.nan,
458
- )
459
- embeds_flat[embed_is_patch.view(-1)] = features
460
-
461
- return embeds_flat.split(num_patches_per_image)
462
-
436
def get_multimodal_embeddings(
437
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
438
image_input = self._parse_and_validate_image_input(**kwargs)
@@ -472,9 +445,9 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
445
return image_features
446
447
return flatten_2d_lists(
475
- self._get_mm_embeds(*args) for args in zip(
448
+ scatter_patch_features(*args) for args in zip(
449
image_features,
477
- image_input["num_patches"],
450
+ image_input["num_embeds"],
451
image_input["embed_is_patch"],
452
))
453
@@ -485,15 +458,10 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
458
) -> torch.Tensor:
459
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
460
if multimodal_embeddings is not None:
488
- # Extract the patch tokens
489
- patch_embeddings = json_map_leaves(
490
- lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
491
- cast(JSONTree[torch.Tensor], multimodal_embeddings),
492
- )
461
inputs_embeds = merge_multimodal_embeddings(
462
input_ids,
463
inputs_embeds,
496
- cast(NestedTensors, patch_embeddings),
464
+ select_patch_features(multimodal_embeddings),
465
self.vision_args.image_token_id,
466
)
467
return inputs_embeds
vllm/model_executor/models/vision.py
+49
-1
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
from abc import ABC, abstractmethod
4
-from typing import Final, Generic, Optional, Protocol, TypeVar, Union
4
+from typing import Final, Generic, Optional, Protocol, TypeVar, Union, cast
5
6
import torch
7
from transformers import PretrainedConfig
@@ -9,9 +9,12 @@ from transformers import PretrainedConfig
9
import vllm.envs as envs
10
from vllm.attention.selector import (backend_name_to_enum,
11
get_global_forced_attn_backend)
12
+from vllm.jsontree import JSONTree, json_map_leaves
13
from vllm.logger import init_logger
14
from vllm.platforms import _Backend, current_platform
15
16
+from .interfaces import MultiModalEmbeddings
17
+
18
logger = init_logger(__name__)
19
20
_C = TypeVar("_C", bound=PretrainedConfig)
@@ -148,3 +151,48 @@ def resolve_visual_encoder_outputs(
151
if post_layer_norm is not None and uses_last_layer:
152
hs_pool[-1] = post_layer_norm(encoder_outputs)
153
return torch.cat(hs_pool, dim=-1)
154
+
155
+
156
+def scatter_patch_features(
157
+ features: torch.Tensor,
158
+ num_embeds: torch.Tensor,
159
+ embed_is_patch: torch.Tensor,
160
+) -> tuple[torch.Tensor, ...]:
161
+ """
162
+ Scatter the patch features into a contiguous tensor that corresponds
163
+ to the embedding tokens defined by the multimodal processor.
164
+
165
+ The rest of the values in the tensor are set to NaN so that they
166
+ can be filtered out by :func`select_patch_features`.
167
+
168
+ Args:
169
+ features: The patch features, concatenated across each image.
170
+ Shape: `(num_patch, feature_depth)`
171
+ num_embeds: The number of image embeddings for each image.
172
+ Shape: `(num_images,)`
173
+ embed_is_patch: A boolean mask indicating which image embeddings
174
+ correspond to patch tokens for each image.
175
+ Shape: `(num_images, num_embeds)`
176
+ """
177
+ num_embeds_per_image: list[int] = num_embeds.tolist()
178
+
179
+ embeds_flat = features.new_full(
180
+ (sum(num_embeds_per_image), features.shape[-1]),
181
+ fill_value=torch.nan,
182
+ )
183
+ embeds_flat[embed_is_patch.view(-1)] = features.flatten(0, -2)
184
+
185
+ return embeds_flat.split(num_embeds_per_image)
186
+
187
+
188
+def select_patch_features(
189
+ multimodal_embeddings: MultiModalEmbeddings) -> MultiModalEmbeddings:
190
+ """
191
+ Given the outputs of :func:`scatter_patch_features`, return only
192
+ the values that correspond to patch features.
193
+ """
194
+ selected_features = json_map_leaves(
195
+ lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
196
+ cast(JSONTree[torch.Tensor], multimodal_embeddings),
197
+ )
198
+ return cast(MultiModalEmbeddings, selected_features)
vllm/multimodal/processing.py
+65
-22
@@ -511,8 +511,35 @@ def iter_token_matches(
511
start_idx += 1
512
513
514
+def replace_token_matches(
515
+ token_ids: list[int],
516
+ match_ids: list[int],
517
+ new_ids: list[int],
518
+) -> list[int]:
519
+ """
520
+ Replace each occurrence of :code:`match_ids` in :code:`token_ids`
521
+ with :code:`new_ids`.
522
+
523
+ Note that empty matches are ignored.
524
+ """
525
+ out_seqs = list[list[int]]()
526
+ prev_end_idx = 0
527
+
528
+ for match in iter_token_matches(token_ids, match_ids):
529
+ start_idx = match.start_idx
530
+ end_idx = match.end_idx
531
+
532
+ out_seqs.append(token_ids[prev_end_idx:start_idx])
533
+ out_seqs.append(new_ids)
534
+ prev_end_idx = end_idx
535
+
536
+ out_seqs.append(token_ids[prev_end_idx:])
537
+
538
+ return flatten_2d_lists(out_seqs)
539
+
540
+
541
@dataclass(repr=False)
515
-class _PromptTargetMatch(ABC):
542
+class PromptTargetMatch(ABC):
543
_origin: BoundPromptUpdate
544
545
@property
@@ -535,7 +562,7 @@ class _PromptTargetMatch(ABC):
562
563
564
@dataclass(repr=False)
538
-class _PromptTargetIndexMatch(_PromptTargetMatch):
565
+class _PromptTargetIndexMatch(PromptTargetMatch):
566
match_idx: int
567
568
@property
@@ -548,7 +575,7 @@ class _PromptTargetIndexMatch(_PromptTargetMatch):
575
576
577
@dataclass(repr=False)
551
-class _PromptTargetTokenMatch(_PromptTargetMatch):
578
+class _PromptTargetTokenMatch(PromptTargetMatch):
579
match: _TokenMatch
580
581
@property
@@ -561,7 +588,7 @@ class _PromptTargetTokenMatch(_PromptTargetMatch):
588
589
590
@dataclass(repr=False)
564
-class _PromptTargetTextMatch(_PromptTargetMatch):
591
+class _PromptTargetTextMatch(PromptTargetMatch):
592
match: re.Match[str]
593
594
@property
@@ -594,7 +621,7 @@ class PlaceholderFeaturesInfo:
621
def find_token_matches(
622
prompt: list[int],
623
prompt_updates: Sequence[BoundPromptUpdate],
597
-) -> Sequence[_PromptTargetMatch]:
624
+) -> Sequence[PromptTargetMatch]:
625
"""Return each target of :code:`prompt_updates` found in :code:`prompt`."""
626
627
def get_matches(update: BoundPromptUpdate):
@@ -620,7 +647,7 @@ def find_token_matches(
647
def find_text_matches(
648
prompt: str,
649
prompt_updates: Sequence[BoundPromptUpdate],
623
-) -> Sequence[_PromptTargetMatch]:
650
+) -> Sequence[PromptTargetMatch]:
651
"""Return each target of :code:`prompt_updates` found in :code:`prompt`."""
652
653
def get_matches(update: BoundPromptUpdate):
@@ -645,15 +672,15 @@ def find_text_matches(
672
673
def _resolve_matches(
674
prompt: PromptSeq,
648
- mm_matches: Mapping[str, Sequence[_PromptTargetMatch]],
649
-) -> list[_PromptTargetMatch]:
675
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
676
+) -> list[PromptTargetMatch]:
677
"""
678
Resolve :code:`mm_matches` to ensure that there are no overlapping matches,
679
and sort them such that earlier matches take priority over later ones.
680
"""
681
matches = [m for matches in mm_matches.values() for m in matches]
682
656
- seen_matches: list[Optional[_PromptTargetMatch]] = [None] * len(prompt)
683
+ seen_matches: list[Optional[PromptTargetMatch]] = [None] * len(prompt)
684
685
for match in matches:
686
for idx in range(match.start_idx, match.end_idx):
@@ -669,7 +696,7 @@ def _resolve_matches(
696
697
def _apply_matches(
698
prompt: _S,
672
- mm_matches: Mapping[str, Sequence[_PromptTargetMatch]],
699
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
700
mm_item_counts: Mapping[str, int],
701
) -> list[_S]:
702
"""Apply the updates in :code:`mm_matches` to :code:`prompt`."""
@@ -718,7 +745,7 @@ def _apply_matches(
745
746
def apply_token_matches(
747
prompt: list[int],
721
- mm_matches: Mapping[str, Sequence[_PromptTargetMatch]],
748
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
749
mm_item_counts: Mapping[str, int],
750
) -> list[int]:
751
"""Apply the updates in :code:`mm_matches` to :code:`prompt`."""
@@ -732,7 +759,7 @@ def apply_token_matches(
759
760
def apply_text_matches(
761
prompt: str,
735
- mm_matches: Mapping[str, Sequence[_PromptTargetMatch]],
762
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
763
mm_item_counts: Mapping[str, int],
764
) -> str:
765
"""Apply the updates in :code:`mm_matches` to :code:`prompt`."""
@@ -1055,14 +1082,14 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1082
Given the original multi-modal items for this modality
1083
and HF-processed data, output the updates to perform.
1084
1058
- Notes:
1059
- - You should not assume that HF processor always performs prompt
1060
- updates: in :meth:`_apply_hf_processor_missing`, this method
1061
- is called on text-only and multimodal-only inputs separately,
1062
- instead of passing them in the same call.
1063
- - The update information returned by this method is also used to
1064
- determine the placeholder token positions for each multi-modal
1065
- item.
1085
+ The information returned by this method is used to update token inputs
1086
+ which bypass the HF processor. It is also used to update the output of
1087
+ HF processor if the HF process does not apply prompt updates to text
1088
+ inputs.
1089
+
1090
+ Moreover, this information is critical to determine the token positions
1091
+ in order to construct :class:`~vllm-multimodal.input.PlaceholderRange`
1092
+ for each multi-modal item.
1093
"""
1094
raise NotImplementedError
1095
@@ -1357,6 +1384,22 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1384
it = (update.bind(tokenizer) for update in prompt_updates)
1385
return dict(full_groupby_modality(it))
1386
1387
+ def _apply_token_matches(
1388
+ self,
1389
+ prompt: list[int],
1390
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
1391
+ mm_item_counts: Mapping[str, int],
1392
+ ) -> list[int]:
1393
+ return apply_token_matches(prompt, mm_matches, mm_item_counts)
1394
+
1395
+ def _apply_text_matches(
1396
+ self,
1397
+ prompt: str,
1398
+ mm_matches: Mapping[str, Sequence[PromptTargetMatch]],
1399
+ mm_item_counts: Mapping[str, int],
1400
+ ) -> str:
1401
+ return apply_text_matches(prompt, mm_matches, mm_item_counts)
1402
+
1403
def _apply_prompt_updates(
1404
self,
1405
token_ids: list[int],
@@ -1388,7 +1431,7 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1431
mm_match_counts.get(modality, 0) >= item_count
1432
for modality, item_count in mm_item_counts.items()
1433
): # yapf: disable
1391
- token_ids = apply_token_matches(
1434
+ token_ids = self._apply_token_matches(
1435
token_ids,
1436
mm_token_matches,
1437
mm_item_counts,
@@ -1406,7 +1449,7 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1449
modality: find_text_matches(text, updates)
1450
for modality, updates in mm_prompt_updates.items()
1451
}
1409
- text = apply_text_matches(
1452
+ text = self._apply_text_matches(
1453
text,
1454
mm_text_matches,
1455
mm_item_counts,