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
[VLM] Merged multi-modal processor for Pixtral (#12211)
Signed-off-by: remi <remi@mistral.ai> Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk> Co-authored-by: DarkLight1337 <tlleungac@connect.ust.hk>
Rémi Delacourt committed
Mar 15, 2025 at 14:28 UTC
61c6a5a79664882a8ab1c9af3ff78677911516dc
9 files changed
+622
-360
examples/offline_inference/pixtral.py
+18
-6
@@ -43,12 +43,18 @@ from vllm.sampling_params import SamplingParams
43
# python demo.py advanced
44
45
46
-def run_simple_demo():
46
+def run_simple_demo(args: argparse.Namespace):
47
model_name = "mistralai/Pixtral-12B-2409"
48
sampling_params = SamplingParams(max_tokens=8192)
49
50
- # Lower max_num_seqs or max_model_len on low-VRAM GPUs.
51
- llm = LLM(model=model_name, tokenizer_mode="mistral")
50
+ # Lower max_model_len and/or max_num_seqs on low-VRAM GPUs.
51
+ llm = LLM(
52
+ model=model_name,
53
+ tokenizer_mode="mistral",
54
+ max_model_len=4096,
55
+ max_num_seqs=2,
56
+ disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
57
+ )
58
59
prompt = "Describe this image in one sentence."
60
image_url = "https://picsum.photos/id/237/200/300"
@@ -76,7 +82,7 @@ def run_simple_demo():
82
print(outputs[0].outputs[0].text)
83
84
79
-def run_advanced_demo():
85
+def run_advanced_demo(args: argparse.Namespace):
86
model_name = "mistralai/Pixtral-12B-2409"
87
max_img_per_msg = 5
88
max_tokens_per_img = 4096
@@ -87,6 +93,7 @@ def run_advanced_demo():
93
tokenizer_mode="mistral",
94
limit_mm_per_prompt={"image": max_img_per_msg},
95
max_model_len=max_img_per_msg * max_tokens_per_img,
96
+ disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
97
)
98
99
prompt = "Describe the following image."
@@ -153,14 +160,19 @@ def main():
160
help="Specify the demo mode: 'simple' or 'advanced'",
161
)
162
163
+ parser.add_argument(
164
+ '--disable-mm-preprocessor-cache',
165
+ action='store_true',
166
+ help='If True, disables caching of multi-modal preprocessor/mapper.')
167
+
168
args = parser.parse_args()
169
170
if args.mode == "simple":
171
print("Running simple demo...")
160
- run_simple_demo()
172
+ run_simple_demo(args)
173
elif args.mode == "advanced":
174
print("Running advanced demo...")
163
- run_advanced_demo()
175
+ run_advanced_demo(args)
176
177
178
if __name__ == "__main__":
tests/models/multimodal/processing/test_common.py
+149
-51
@@ -2,17 +2,23 @@
2
3
import copy
4
from functools import partial
5
-from typing import Optional
5
+from typing import Optional, Union
6
7
import numpy as np
8
import pytest
9
+from mistral_common.protocol.instruct.messages import (ImageChunk, TextChunk,
10
+ UserMessage)
11
+from mistral_common.protocol.instruct.request import ChatCompletionRequest
12
from PIL import Image
13
+from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast
14
15
from vllm.config import ModelConfig
16
from vllm.inputs import InputProcessingContext
13
-from vllm.multimodal import MULTIMODAL_REGISTRY
14
-from vllm.multimodal.processing import ProcessingCache
15
-from vllm.transformers_utils.tokenizer import cached_tokenizer_from_config
17
+from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalDataDict
18
+from vllm.multimodal.inputs import MultiModalInputs
19
+from vllm.multimodal.processing import BaseMultiModalProcessor, ProcessingCache
20
+from vllm.transformers_utils.tokenizer import (MistralTokenizer,
21
+ cached_tokenizer_from_config)
22
23
from ....multimodal.utils import random_audio, random_image, random_video
24
from ...registry import HF_EXAMPLE_MODELS
@@ -85,14 +91,6 @@ def _test_processing_correctness(
91
partial(random_audio, rng, min_len=512, max_len=1024, sr=16000),
92
}
93
88
- tokenizer_encode_kwargs = {}
89
- if model_config.hf_config.model_type in ("mllama", "whisper", "ultravox"):
90
- # For some multimodal models, tokenizer will always add bos_token
91
- # at the beginning of prompt by default, causing hf_processor outputs
92
- # incorrect token ids. So we need use `add_special_tokens=False` here
93
- # to leave bos_token to be added by the processor.
94
- tokenizer_encode_kwargs = {"add_special_tokens": False}
95
-
94
for batch_idx in range(num_batches):
95
mm_data = {
96
k:
@@ -115,43 +113,131 @@ def _test_processing_correctness(
113
elif len(mm_data[k]) == 1:
114
mm_data[k] = mm_data[k][0]
115
118
- baseline_result = baseline_processor.apply(
119
- prompt,
120
- mm_data=mm_data,
121
- hf_processor_mm_kwargs={},
122
- )
123
- cached_result = cached_processor.apply(
124
- prompt,
125
- mm_data=mm_data,
126
- hf_processor_mm_kwargs={},
127
- )
128
-
129
- assert _drop_mm_kwargs_keys(
130
- baseline_result, ignore_mm_keys) == _drop_mm_kwargs_keys(
131
- cached_result, ignore_mm_keys), (
132
- f"Failed ({batch_idx=}, {prompt=}, {mm_data=})")
133
-
134
- baseline_tokenized_result = baseline_processor.apply(
135
- tokenizer.encode(prompt, **tokenizer_encode_kwargs),
136
- mm_data=mm_data,
137
- hf_processor_mm_kwargs={},
138
- )
139
-
140
- assert _drop_mm_kwargs_keys(
141
- baseline_result, ignore_mm_keys) == _drop_mm_kwargs_keys(
142
- baseline_tokenized_result, ignore_mm_keys), (
143
- f"Failed ({batch_idx=}, {prompt=}, {mm_data=})")
144
-
145
- cached_tokenized_result = cached_processor.apply(
146
- tokenizer.encode(prompt, **tokenizer_encode_kwargs),
147
- mm_data=mm_data,
148
- hf_processor_mm_kwargs={},
149
- )
150
-
151
- assert _drop_mm_kwargs_keys(
152
- cached_result, ignore_mm_keys) == _drop_mm_kwargs_keys(
153
- cached_tokenized_result, ignore_mm_keys), (
154
- f"Failed ({batch_idx=}, {prompt=}, {mm_data=})")
116
+ if isinstance(tokenizer, MistralTokenizer):
117
+ _test_processing_correctness_mistral(
118
+ model_config,
119
+ tokenizer,
120
+ prompt,
121
+ mm_data,
122
+ baseline_processor,
123
+ cached_processor,
124
+ batch_idx,
125
+ ignore_mm_keys=ignore_mm_keys,
126
+ )
127
+ else:
128
+ _test_processing_correctness_hf(
129
+ model_config,
130
+ tokenizer,
131
+ prompt,
132
+ mm_data,
133
+ baseline_processor,
134
+ cached_processor,
135
+ batch_idx,
136
+ ignore_mm_keys=ignore_mm_keys,
137
+ )
138
+
139
+
140
+def _test_processing_correctness_hf(
141
+ model_config: ModelConfig,
142
+ tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
143
+ prompt: str,
144
+ mm_data: MultiModalDataDict,
145
+ baseline_processor: BaseMultiModalProcessor,
146
+ cached_processor: BaseMultiModalProcessor,
147
+ batch_idx: int,
148
+ ignore_mm_keys: Optional[list[str]] = None,
149
+):
150
+ if model_config.hf_config.model_type in ("mllama", "whisper", "ultravox"):
151
+ # For some multimodal models, tokenizer will always add bos_token
152
+ # at the beginning of prompt by default, causing hf_processor outputs
153
+ # incorrect token ids. So we need use `add_special_tokens=False` here
154
+ # to leave bos_token to be added by the processor.
155
+ token_prompt = tokenizer.encode(prompt, add_special_tokens=False)
156
+ else:
157
+ token_prompt = tokenizer.encode(prompt)
158
+
159
+ baseline_result = baseline_processor.apply(
160
+ prompt,
161
+ mm_data=mm_data,
162
+ hf_processor_mm_kwargs={},
163
+ )
164
+ cached_result = cached_processor.apply(
165
+ prompt,
166
+ mm_data=mm_data,
167
+ hf_processor_mm_kwargs={},
168
+ )
169
+
170
+ assert _inputs_equal(
171
+ baseline_result,
172
+ cached_result,
173
+ ignore_mm_keys,
174
+ ), f"Failed ({batch_idx=}, {prompt=}, {mm_data=})"
175
+
176
+ baseline_tokenized_result = baseline_processor.apply(
177
+ token_prompt,
178
+ mm_data=mm_data,
179
+ hf_processor_mm_kwargs={},
180
+ )
181
+
182
+ assert _inputs_equal(
183
+ baseline_result,
184
+ baseline_tokenized_result,
185
+ ignore_mm_keys,
186
+ ), f"Failed ({batch_idx=}, {prompt=}, {mm_data=})"
187
+
188
+ cached_tokenized_result = cached_processor.apply(
189
+ token_prompt,
190
+ mm_data=mm_data,
191
+ hf_processor_mm_kwargs={},
192
+ )
193
+
194
+ assert _inputs_equal(
195
+ cached_result,
196
+ cached_tokenized_result,
197
+ ignore_mm_keys,
198
+ ), f"Failed ({batch_idx=}, {prompt=}, {mm_data=})"
199
+
200
+
201
+def _test_processing_correctness_mistral(
202
+ model_config: ModelConfig,
203
+ tokenizer: MistralTokenizer,
204
+ prompt: str,
205
+ mm_data: MultiModalDataDict,
206
+ baseline_processor: BaseMultiModalProcessor,
207
+ cached_processor: BaseMultiModalProcessor,
208
+ batch_idx: int,
209
+ ignore_mm_keys: Optional[list[str]] = None,
210
+):
211
+ images = mm_data.get("image", [])
212
+ if not isinstance(images, list):
213
+ images = [images]
214
+
215
+ request = ChatCompletionRequest(messages=[
216
+ UserMessage(content=[
217
+ TextChunk(text=prompt),
218
+ *(ImageChunk(image=image) for image in images),
219
+ ]),
220
+ ])
221
+ res = tokenizer.mistral.encode_chat_completion(request)
222
+ token_prompt = res.tokens
223
+
224
+ # Mistral chat outputs tokens directly, rather than text prompts
225
+ baseline_tokenized_result = baseline_processor.apply(
226
+ token_prompt,
227
+ mm_data=mm_data,
228
+ hf_processor_mm_kwargs={},
229
+ )
230
+ cached_tokenized_result = cached_processor.apply(
231
+ token_prompt,
232
+ mm_data=mm_data,
233
+ hf_processor_mm_kwargs={},
234
+ )
235
+
236
+ assert _inputs_equal(
237
+ baseline_tokenized_result,
238
+ cached_tokenized_result,
239
+ ignore_mm_keys,
240
+ ), f"Failed ({batch_idx=}, {prompt=}, {mm_data=})"
241
242
243
# yapf: disable
@@ -173,6 +259,7 @@ def _test_processing_correctness(
259
"llava-hf/llava-onevision-qwen2-0.5b-ov-hf",
260
"meta-llama/Llama-3.2-11B-Vision-Instruct",
261
"TIGER-Lab/Mantis-8B-siglip-llama3",
262
+ "mistralai/Pixtral-12B-2409",
263
"mistral-community/pixtral-12b",
264
"openbmb/MiniCPM-o-2_6",
265
"openbmb/MiniCPM-V-2_6",
@@ -241,8 +328,19 @@ def test_processing_correctness_phi3v(
328
)
329
330
244
-def _drop_mm_kwargs_keys(result: dict,
245
- ignore_mm_keys: Optional[list[str]] = None) -> dict:
331
+def _inputs_equal(
332
+ a: MultiModalInputs,
333
+ b: MultiModalInputs,
334
+ ignore_mm_keys: Optional[list[str]] = None,
335
+):
336
+ return _drop_mm_kwargs_keys(a, ignore_mm_keys) == _drop_mm_kwargs_keys(
337
+ b, ignore_mm_keys)
338
+
339
+
340
+def _drop_mm_kwargs_keys(
341
+ result: MultiModalInputs,
342
+ ignore_mm_keys: Optional[list[str]] = None,
343
+) -> MultiModalInputs:
344
"""Drop specified keys from result['mm_kwargs'].
345
346
This is mainly to avoid doing exact match of audio_features in ultravox.
vllm/model_executor/models/llava.py
+50
-76
@@ -68,23 +68,15 @@ class PixtralHFImagePixelInputs(TypedDict):
68
in which case the data is passed as a list instead of a batched tensor.
69
"""
70
71
- feat_is_patch: Union[torch.Tensor, list[torch.Tensor]]
72
- """
73
- A boolean mask indicating which image features correspond
74
- to patch tokens.
75
-
76
- Shape: `(batch_size, num_crops, num_patch)`
77
- """
78
-
71
embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
72
"""
73
A boolean mask indicating which image embeddings correspond
74
to patch tokens.
75
84
- Shape: `(batch_size, num_embeds)`
76
+ Shape: `(batch_size, num_images, num_embeds)`
77
"""
78
87
- num_crops: Union[torch.Tensor, list[torch.Tensor]]
79
+ num_patches: Union[torch.Tensor, list[torch.Tensor]]
80
"""Shape: `(batch_size, num_images)`"""
81
82
@@ -360,16 +352,16 @@ class PixtralHFMultiModalProcessor(
352
image_height=pixel_value.shape[-2],
353
) for pixel_value in processed_outputs["pixel_values"]
354
]
363
- num_crops = torch.tensor([(ncols + 1) * nrows
364
- for ncols, nrows in tile_sizes])
355
+ num_patches = 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
366
- # flatten the list and later use `num_crops` to get per-image masks.
367
- embed_is_patch = torch.tensor(
368
- flatten_2d_lists([([True] * ncols + [False]) * nrows
369
- for ncols, nrows in tile_sizes]))
370
- processed_outputs["num_crops"] = num_crops
358
+ # later use `num_patches` 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
364
processed_outputs["embed_is_patch"] = embed_is_patch
372
- processed_outputs["feat_is_patch"] = embed_is_patch
365
366
return processed_outputs
367
@@ -378,14 +370,10 @@ class PixtralHFMultiModalProcessor(
370
hf_inputs: BatchFeature,
371
hf_processor_mm_kwargs: Mapping[str, object],
372
) -> Mapping[str, MultiModalFieldConfig]:
381
- num_crops = hf_inputs.get("num_crops", torch.empty(0)).view(-1)
373
return dict(
383
- feat_is_patch=MultiModalFieldConfig.flat_from_sizes(
384
- "image", num_crops),
385
- embed_is_patch=MultiModalFieldConfig.flat_from_sizes(
386
- "image", num_crops),
387
- num_crops=MultiModalFieldConfig.batched("image"),
374
pixel_values=MultiModalFieldConfig.batched("image"),
375
+ num_patches=MultiModalFieldConfig.batched("image"),
376
+ embed_is_patch=MultiModalFieldConfig.batched("image"),
377
image_embeds=MultiModalFieldConfig.batched("image"),
378
)
379
@@ -628,27 +616,21 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
616
f"Got type: {type(pixel_values)}")
617
618
if self.config.vision_config.model_type == "pixtral":
631
- feat_is_patch = kwargs.pop("feat_is_patch")
632
- if not isinstance(feat_is_patch, (torch.Tensor, list)):
633
- raise ValueError("Incorrect type of feat_is_patch. "
634
- f"Got type: {type(feat_is_patch)}")
635
-
619
embed_is_patch = kwargs.pop("embed_is_patch")
620
if not isinstance(embed_is_patch, (torch.Tensor, list)):
621
raise ValueError("Incorrect type of embed_is_patch. "
622
f"Got type: {type(embed_is_patch)}")
623
641
- num_crops = kwargs.pop("num_crops")
642
- if not isinstance(num_crops, (torch.Tensor, list)):
643
- raise ValueError("Incorrect type of num_crops. "
644
- f"Got type: {type(num_crops)}")
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)}")
628
629
return PixtralHFImagePixelInputs(
630
type="pixel_values_pixtral",
631
pixel_values=flatten_bn(pixel_values),
649
- feat_is_patch=feat_is_patch,
632
embed_is_patch=embed_is_patch,
651
- num_crops=num_crops,
633
+ num_patches=num_patches,
634
)
635
636
return LlavaImagePixelInputs(
@@ -687,21 +669,26 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
669
vision_tower: Union[CLIPVisionModel, SiglipVisionModel,
670
PixtralHFVisionModel],
671
pixel_values: Union[torch.Tensor, list[torch.Tensor]],
690
- ) -> torch.Tensor:
691
-
672
+ ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
673
# NOTE: we skip the step to select the vision feature layer since
674
# this is already done inside the vision tower
675
image_features = vision_tower(pixel_values)
676
696
- return self._select_image_features(
697
- image_features,
698
- strategy=self.config.vision_feature_select_strategy,
677
+ def select_features(leaf: torch.Tensor):
678
+ return self._select_image_features(
679
+ leaf,
680
+ strategy=self.config.vision_feature_select_strategy,
681
+ )
682
+
683
+ return cast(
684
+ Union[torch.Tensor, tuple[torch.Tensor, ...]],
685
+ json_map_leaves(select_features, image_features),
686
)
687
688
def _process_image_pixels(
689
self,
690
inputs: Union[LlavaImagePixelInputs, PixtralHFImagePixelInputs],
704
- ) -> torch.Tensor:
691
+ ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
692
assert self.vision_tower is not None
693
694
pixel_values = inputs["pixel_values"]
@@ -731,45 +718,30 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
718
719
def _get_mm_embeds(
720
self,
734
- features: torch.Tensor, # Shape: (num_crop, num_patch, d)
735
- feat_is_patch: torch.Tensor, # Shape: (num_crop, num_patch)
736
- num_crops: torch.Tensor, # Shape: (num_images,)
737
- embed_is_patch: torch.Tensor, # Shape: (num_embeds,)
738
- ) -> list[torch.Tensor]:
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
"""
744
-
745
- # Insert columns of nan values according to `feat_is_patch`. This work
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`.
751
- feat_is_patch = feat_is_patch.view(-1)
752
- embed_is_patch = embed_is_patch.view(-1)
753
- expanded_embedding = torch.full(
754
- (sum(num_crops), *features.shape[1:]),
755
- torch.nan,
756
- dtype=features.dtype).to(features.device)
757
- expanded_embedding[feat_is_patch] = features
736
+ num_patches_per_image: list[int] = num_patches.tolist()
737
759
- num_crops_per_image = num_crops.tolist()
760
- feats_per_image = expanded_embedding.split(num_crops_per_image)
761
- f_is_patch_per_image = feat_is_patch.split(num_crops_per_image)
762
-
763
- embed_dim = expanded_embedding.shape[-1]
764
- num_embeds = embed_is_patch.shape[0]
765
-
766
- embeds_in_batch = list[torch.Tensor]()
767
- for feats, f_is_patch in zip(feats_per_image, f_is_patch_per_image):
768
- embeds = feats.new_full((num_embeds, embed_dim), torch.nan)
769
- embeds[embed_is_patch] = feats[f_is_patch]
770
- embeds_in_batch.append(embeds)
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
772
- return embeds_in_batch
744
+ return embeds_flat.split(num_patches_per_image)
745
746
def get_multimodal_embeddings(
747
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
@@ -784,12 +756,12 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
756
# The path is used for pixtral (V0 only) and llava (V0/V1)
757
return vision_embeddings
758
787
- nested_emb = [
759
+ return flatten_2d_lists(
760
self._get_mm_embeds(*args) for args in zip(
789
- vision_embeddings, image_input["feat_is_patch"],
790
- image_input["num_crops"], image_input["embed_is_patch"])
791
- ]
792
- return flatten_2d_lists(nested_emb)
761
+ vision_embeddings,
762
+ image_input["num_patches"],
763
+ image_input["embed_is_patch"],
764
+ ))
765
766
def get_input_embeddings(
767
self,
@@ -805,9 +777,11 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
777
)
778
779
inputs_embeds = merge_multimodal_embeddings(
808
- input_ids, inputs_embeds, cast(NestedTensors,
809
- patch_embeddings),
810
- self.config.image_token_index)
780
+ input_ids,
781
+ inputs_embeds,
782
+ cast(NestedTensors, patch_embeddings),
783
+ self.config.image_token_index,
784
+ )
785
return inputs_embeds
786
787
def forward(
vllm/model_executor/models/molmo.py
+2
-4
@@ -1585,15 +1585,13 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1585
1586
image_features = self._process_image_input(image_input)
1587
1588
- nested_embeds = [
1588
+ return flatten_2d_lists(
1589
self._get_mm_embeds(*args) for args in zip(
1590
image_features,
1591
image_input["feat_is_patch"],
1592
image_input["num_crops"],
1593
image_input["embed_is_patch"],
1594
- )
1595
- ]
1596
- return flatten_2d_lists(nested_embeds)
1594
+ ))
1595
1596
def get_input_embeddings(
1597
self,
vllm/model_executor/models/paligemma.py
+4
-5
@@ -1,7 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
-
3
-from typing import (Iterable, Literal, Mapping, Optional, Set, Tuple,
4
- TypedDict, Union)
2
+from collections.abc import Iterable, Mapping, Sequence
3
+from typing import Literal, Optional, Set, Tuple, TypedDict, Union
4
5
import torch
6
from torch import nn
@@ -17,7 +16,7 @@ from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
16
from vllm.multimodal.parse import MultiModalDataItems
17
from vllm.multimodal.processing import (BaseMultiModalProcessor,
18
BaseProcessingInfo, PromptIndexTargets,
20
- PromptInsertion, PromptReplacement,
19
+ PromptInsertion, PromptUpdate,
20
PromptUpdateDetails)
21
from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
22
from vllm.sequence import IntermediateTensors
@@ -144,7 +143,7 @@ class PaliGemmaMultiModalProcessor(
143
mm_items: MultiModalDataItems,
144
hf_processor_mm_kwargs: Mapping[str, object],
145
out_mm_kwargs: MultiModalKwargs,
147
- ) -> list[PromptReplacement]:
146
+ ) -> Sequence[PromptUpdate]:
147
hf_config = self.info.get_hf_config()
148
image_token_id = hf_config.image_token_index
149
vllm/model_executor/models/pixtral.py
+374
-208
@@ -1,26 +1,28 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
import math
4
-from collections.abc import Iterable, Mapping
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, Optional, Set, Tuple, Union
7
+from typing import List, Literal, Optional, Set, Tuple, TypedDict, Union, cast
8
9
import torch
10
import torch.nn as nn
11
import torch.nn.functional as F
12
from mistral_common.protocol.instruct.messages import ImageChunk
13
+from mistral_common.tokens.tokenizers.multimodal import ImageEncoder
14
from PIL import Image
14
-from transformers import PixtralVisionConfig
15
+from transformers import PixtralVisionConfig, TensorType
16
+from transformers.image_utils import ImageInput
17
from transformers.models.pixtral.image_processing_pixtral import (
18
_num_image_tokens as _get_pixtral_hf_num_image_tokens)
19
from transformers.models.pixtral.modeling_pixtral import (
20
PixtralRotaryEmbedding, apply_rotary_pos_emb, position_ids_in_meshgrid)
21
+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
22
-from vllm.inputs import (INPUT_REGISTRY, DecoderOnlyInputs, DummyData,
23
- InputContext, token_inputs)
25
+from vllm.jsontree import JSONTree, json_map_leaves
26
from vllm.model_executor.layers.activation import get_act_and_mul_fn
27
from vllm.model_executor.layers.layernorm import RMSNorm
28
from vllm.model_executor.layers.linear import (MergedColumnParallelLinear,
@@ -31,13 +33,20 @@ from vllm.model_executor.layers.sampler import SamplerOutput, get_sampler
33
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
34
from vllm.model_executor.sampling_metadata import SamplingMetadata
35
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalKwargs
34
-from vllm.multimodal.inputs import PlaceholderRange
35
-from vllm.multimodal.utils import consecutive_placeholder_ranges
36
-from vllm.sequence import IntermediateTensors, SequenceData
37
-from vllm.transformers_utils.tokenizer import cached_tokenizer_from_config
36
+from vllm.multimodal.inputs import MultiModalFieldConfig, NestedTensors
37
+from vllm.multimodal.parse import (ImageProcessorItems, ImageSize,
38
+ MultiModalDataItems)
39
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
40
+ BaseProcessingInfo, PromptReplacement,
41
+ PromptUpdate)
42
+from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
43
+from vllm.sequence import IntermediateTensors
44
+from vllm.transformers_utils.tokenizer import (MistralTokenizer,
45
+ cached_tokenizer_from_config)
46
+from vllm.utils import flatten_2d_lists
47
48
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
40
-from .utils import (init_vllm_registered_model, maybe_prefix,
49
+from .utils import (flatten_bn, init_vllm_registered_model, maybe_prefix,
50
merge_multimodal_embeddings)
51
from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
52
@@ -48,132 +57,275 @@ except ImportError:
57
USE_XFORMERS_OPS = False
58
59
51
-def get_max_pixtral_image_tokens(ctx: InputContext):
52
- tokenizer = cached_tokenizer_from_config(ctx.model_config)
53
- mm_encoder = tokenizer.instruct.mm_encoder
60
+class PixtralImagePixelInputs(TypedDict):
61
+ type: Literal["pixel_values"]
62
55
- image_config = mm_encoder.mm_config if hasattr(
56
- mm_encoder, "mm_config") else mm_encoder.image_config
63
+ images: Union[torch.Tensor, list[torch.Tensor]]
64
+ """
65
+ Shape: `(batch_size * num_images, num_channels, image_width, image_height)`
66
58
- max_image_size = image_config.max_image_size
59
- image_patch_size = image_config.image_patch_size
67
+ The result of stacking :attr:`ImageEncoding.tokens` from each prompt.
68
+ """
69
61
- return ((max_image_size // image_patch_size)**2)
70
+ embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
71
+ """
72
+ A boolean mask indicating which image embeddings correspond
73
+ to patch tokens.
74
+
75
+ Shape: `(batch_size, num_images, num_embeds)`
76
+ """
77
78
+ num_patches: Union[torch.Tensor, list[torch.Tensor]]
79
+ """Shape: `(batch_size, num_images)`"""
80
64
-def dummy_data_for_pixtral(ctx: InputContext, seq_len: int,
65
- mm_counts: Mapping[str, int]):
66
- tokenizer = cached_tokenizer_from_config(ctx.model_config)
81
68
- mm_encoder = tokenizer.mistral.instruct_tokenizer.mm_encoder
69
- image_token_id = mm_encoder.special_ids.img
82
+class PixtralProcessorAdapter:
83
+ """
84
+ Provide a HF-compatible interface for
85
+ :class:`mistral_common.tokens.tokenizers.multimodal.ImageEncoder`.
86
+ """
87
71
- mm_config = ctx.get_mm_config()
72
- num_images = mm_config.get_limit_per_prompt("image")
88
+ def __init__(self, tokenizer: MistralTokenizer) -> None:
89
+ super().__init__()
90
74
- # dummy size
75
- size = 256
76
- image = Image.new("RGB", (size, size), color=0)
91
+ self.tokenizer = tokenizer
92
78
- encoding = tokenizer.instruct.mm_encoder(ImageChunk(image=image))
79
- image_feature_size = len(encoding.tokens)
80
- num_image_tokens = image_feature_size * num_images
81
- seq_data = SequenceData.from_prompt_token_counts(
82
- (image_token_id, num_image_tokens),
83
- (0, seq_len - num_image_tokens),
84
- )
93
+ @property
94
+ def image_processor(self) -> ImageEncoder:
95
+ image_encoder = self.tokenizer.instruct.mm_encoder
96
+ assert isinstance(image_encoder, ImageEncoder)
97
+ return image_encoder
98
86
- mm_data = {"image": num_images * [image]}
87
- mm_placeholders = {
88
- "image":
89
- consecutive_placeholder_ranges(num_items=num_images,
90
- item_size=image_feature_size)
91
- }
92
- return DummyData(seq_data, mm_data, mm_placeholders)
99
+ @cached_property
100
+ def image_break_id(self) -> int:
101
+ return self.image_processor.special_ids.img_break
102
103
+ @cached_property
104
+ def image_token_id(self) -> int:
105
+ return self.image_processor.special_ids.img
106
95
-def input_mapper_for_pixtral(ctx: InputContext,
96
- data: object) -> MultiModalKwargs:
97
- """Maps the input data to its MultiModalKwargs (if any).
107
+ @cached_property
108
+ def image_end_id(self) -> int:
109
+ return self.image_processor.special_ids.img_end
110
99
- Args:
100
- ctx: Context of the loaded model.
101
- data: data potentially containing PIL images to be processed
102
- and mapped to `images`.
111
+ @cached_property
112
+ def image_size(self) -> int:
113
+ return self.image_processor.mm_config.max_image_size
114
104
- Returns:
105
- MultiModalKwargs containing the stacked normalized images tensor or
106
- image embeddings.
107
- """
108
- tokenizer = cached_tokenizer_from_config(ctx.model_config)
109
-
110
- data_list = data if isinstance(data, list) else [data]
111
-
112
- images = []
113
- image_tokens_list = []
114
- for image_data in data_list:
115
- image = ImageChunk(image=image_data)
116
- encoding = tokenizer.instruct.mm_encoder(image)
117
- image = torch.from_numpy(encoding.image).to(dtype=torch.float16)
118
- images.append(image)
119
- image_tokens_list.append(encoding.tokens)
120
-
121
- image_tokens = torch.tensor([
122
- token_id for image_tokens in image_tokens_list
123
- for token_id in image_tokens
124
- ])
125
- return MultiModalKwargs({"images": images, "image_tokens": image_tokens})
126
-
127
-
128
-def input_processor_for_pixtral(ctx: InputContext, inputs: DecoderOnlyInputs):
129
- multi_modal_data = inputs.get("multi_modal_data")
130
- if multi_modal_data is None or "image" not in multi_modal_data:
131
- return inputs
132
-
133
- prompt_token_ids = inputs.get("prompt_token_ids")
134
- prompt = inputs.get("prompt")
135
- tokenizer = cached_tokenizer_from_config(ctx.model_config)
136
-
137
- mm_encoder = tokenizer.mistral.instruct_tokenizer.mm_encoder
138
- image_token_id = mm_encoder.special_ids.img
139
- image_break_id = mm_encoder.special_ids.img_break
140
- image_end_id = mm_encoder.special_ids.img_end
141
-
142
- if image_token_id not in inputs['prompt_token_ids']:
143
- raise ValueError(
144
- f"You've passed {inputs=} without {image_token_id=}"
145
- " Make sure to process your input via mistral_common's"
146
- " tokenizer or pass a chat completion request. For more"
147
- " For more info, see: "
148
- "https://github.com/vllm-project/vllm/issues/8411.")
149
-
150
- # Get precise tracking of placeholder positions
151
- placeholder_ranges = []
152
- curr_offset = -1
153
- curr_length = 0
154
- for i in range(len(prompt_token_ids)):
155
- if prompt_token_ids[i] in (image_token_id, image_break_id):
156
- if curr_offset < 0:
157
- curr_offset = i
158
- curr_length += 1
159
- elif prompt_token_ids[i] == image_end_id:
160
- curr_length += 1
161
- placeholder_ranges.append(
162
- PlaceholderRange(offset=curr_offset, length=curr_length))
163
- curr_offset = -1
164
- curr_length = 0
165
- else:
166
- pass
167
- return token_inputs(prompt=prompt,
168
- prompt_token_ids=prompt_token_ids,
169
- multi_modal_data=multi_modal_data,
170
- multi_modal_placeholders={"image": placeholder_ranges})
115
+ @cached_property
116
+ def patch_size(self) -> int:
117
+ return self.image_processor.mm_config.image_patch_size
118
+
119
+ def __call__(
120
+ self,
121
+ text: Optional[Union[TextInput, list[TextInput]]] = None,
122
+ images: Optional[Union[ImageInput, list[ImageInput]]] = None,
123
+ return_tensors: Optional[Union[str, TensorType]] = None,
124
+ **kwargs,
125
+ ) -> Mapping[str, NestedTensors]:
126
+ if text is None:
127
+ text = []
128
+ if not isinstance(text, list):
129
+ text = [text]
130
+ if images is None:
131
+ images = []
132
+ if not isinstance(images, list):
133
+ images = [images]
134
+
135
+ if not images:
136
+ input_ids = self.tokenizer(text).input_ids
137
+
138
+ return {"input_ids": torch.tensor(input_ids)}
139
+
140
+ # Allow dummy text, which is used for profiling as well as token inputs
141
+ if any(len(t) > 0 for t in text):
142
+ raise ValueError(
143
+ "You've passed text inputs instead of token inputs. "
144
+ "Make sure to process your input via `mistral_common`'s "
145
+ "tokenizer or pass a chat completion request. "
146
+ "For more info, see: "
147
+ "https://github.com/vllm-project/vllm/issues/8411.")
148
+
149
+ image_token_id = self.image_token_id
150
+
151
+ images_processed = list[torch.Tensor]()
152
+ images_tokens = list[torch.Tensor]()
153
+ images_embed_is_patch = list[torch.Tensor]()
154
+ images_num_patches = list[int]()
155
+
156
+ for image in images:
157
+ image_inputs = self.image_processor(ImageChunk(image=image))
158
+
159
+ image_processed = torch.tensor(image_inputs.image)
160
+ image_tokens = torch.tensor(image_inputs.tokens)
161
+
162
+ images_processed.append(image_processed)
163
+ images_tokens.append(image_tokens)
164
+ images_embed_is_patch.append(image_tokens == image_token_id)
165
+ images_num_patches.append(len(image_tokens))
166
+
167
+ return {
168
+ "input_ids": torch.cat(images_tokens)[None].expand(len(text), -1),
169
+ "images": images_processed,
170
+ "embed_is_patch": images_embed_is_patch,
171
+ "num_patches": torch.tensor(images_num_patches),
172
+ }
173
+
174
+
175
+class PixtralProcessingInfo(BaseProcessingInfo):
176
+
177
+ def get_tokenizer(self) -> MistralTokenizer:
178
+ tokenizer = cached_tokenizer_from_config(self.ctx.model_config)
179
+ if not isinstance(tokenizer, MistralTokenizer):
180
+ raise ValueError("This model requires `--tokenizer-mode mistral`")
181
+
182
+ return tokenizer
183
+
184
+ def get_hf_processor(self) -> PixtralProcessorAdapter:
185
+ return PixtralProcessorAdapter(self.get_tokenizer())
186
+
187
+ def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
188
+ return {"image": None}
189
+
190
+ def get_mm_max_tokens_per_item(
191
+ self,
192
+ seq_len: int,
193
+ mm_counts: Mapping[str, int],
194
+ ) -> Mapping[str, int]:
195
+ return {"image": self.get_max_image_tokens()}
196
+
197
+ def get_vision_config(
198
+ self,
199
+ processor: Optional[PixtralProcessorAdapter] = None,
200
+ ):
201
+ if processor is None:
202
+ processor = self.get_hf_processor()
203
+
204
+ return PixtralVisionConfig(
205
+ image_size=processor.image_size,
206
+ patch_size=processor.patch_size,
207
+ )
208
+
209
+ def get_num_image_tokens(
210
+ self,
211
+ *,
212
+ image_width: int,
213
+ image_height: int,
214
+ processor: Optional[PixtralProcessorAdapter] = None,
215
+ ) -> int:
216
+ if processor is None:
217
+ processor = self.get_hf_processor()
218
+
219
+ ncols, nrows = processor.image_processor._image_to_num_tokens(
220
+ Image.new("RGB", (image_width, image_height)))
221
+
222
+ return (ncols + 1) * nrows
223
+
224
+ def get_image_size_with_most_features(self) -> ImageSize:
225
+ image_processor = self.get_hf_processor().image_processor
226
+ max_image_size = image_processor.mm_config.max_image_size
227
+
228
+ return ImageSize(width=max_image_size, height=max_image_size)
229
+
230
+ def get_max_image_tokens(self) -> int:
231
+ target_width, target_height = self.get_image_size_with_most_features()
232
+
233
+ return self.get_num_image_tokens(
234
+ image_width=target_width,
235
+ image_height=target_height,
236
+ )
237
+
238
+
239
+class PixtralDummyInputsBuilder(BaseDummyInputsBuilder[PixtralProcessingInfo]):
240
+
241
+ def get_dummy_processor_inputs(
242
+ self,
243
+ seq_len: int,
244
+ mm_counts: Mapping[str, int],
245
+ ) -> ProcessorInputs:
246
+ num_images = mm_counts.get("image", 0)
247
+
248
+ target_width, target_height = \
249
+ self.info.get_image_size_with_most_features()
250
+
251
+ mm_data = {
252
+ "image":
253
+ self._get_dummy_images(width=target_width,
254
+ height=target_height,
255
+ num_images=num_images)
256
+ }
257
+
258
+ return ProcessorInputs(
259
+ prompt_text="",
260
+ mm_data=mm_data,
261
+ )
262
+
263
+
264
+class PixtralMultiModalProcessor(BaseMultiModalProcessor[PixtralProcessingInfo]
265
+ ):
266
267
+ def _get_mm_fields_config(
268
+ self,
269
+ hf_inputs: Mapping[str, NestedTensors],
270
+ hf_processor_mm_kwargs: Mapping[str, object],
271
+ ) -> Mapping[str, MultiModalFieldConfig]:
272
+ return dict(
273
+ images=MultiModalFieldConfig.batched("image"),
274
+ embed_is_patch=MultiModalFieldConfig.batched("image"),
275
+ num_patches=MultiModalFieldConfig.batched("image"),
276
+ )
277
+
278
+ def _get_prompt_updates(
279
+ self,
280
+ mm_items: MultiModalDataItems,
281
+ hf_processor_mm_kwargs: Mapping[str, object],
282
+ out_mm_kwargs: MultiModalKwargs,
283
+ ) -> Sequence[PromptUpdate]:
284
+ processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
285
+
286
+ image_break_id = processor.image_break_id
287
+ image_token_id = processor.image_token_id
288
+ image_end_id = processor.image_end_id
289
+
290
+ def get_replacement(item_idx: int):
291
+ images = mm_items.get_items("image", ImageProcessorItems)
292
+ image_size = images.get_image_size(item_idx)
293
+
294
+ ncols, nrows = processor.image_processor._image_to_num_tokens(
295
+ Image.new("RGB", (image_size.width, image_size.height)))
296
+
297
+ tokens = ([image_token_id] * ncols + [image_break_id]) * nrows
298
+ tokens[-1] = image_end_id
299
+
300
+ return tokens
301
+
302
+ return [
303
+ PromptReplacement(
304
+ modality="image",
305
+ target="", # Never match the prompt (see below note)
306
+ replacement=get_replacement,
307
+ ),
308
+ ]
309
+
310
+ def _cached_apply_hf_processor(
311
+ self,
312
+ prompt: Union[str, list[int]],
313
+ mm_data_items: MultiModalDataItems,
314
+ hf_processor_mm_kwargs: Mapping[str, object],
315
+ ) -> tuple[list[int], MultiModalKwargs, bool]:
316
+ prompt_ids, mm_kwargs, _ = super()._cached_apply_hf_processor(
317
+ prompt=prompt,
318
+ mm_data_items=mm_data_items,
319
+ hf_processor_mm_kwargs=hf_processor_mm_kwargs,
320
+ )
321
+
322
+ # NOTE: The tokens are already inserted by the chat template
323
+ return prompt_ids, mm_kwargs, True
324
173
-@MULTIMODAL_REGISTRY.register_image_input_mapper(input_mapper_for_pixtral)
174
-@MULTIMODAL_REGISTRY.register_max_image_tokens(get_max_pixtral_image_tokens)
175
-@INPUT_REGISTRY.register_dummy_data(dummy_data_for_pixtral)
176
-@INPUT_REGISTRY.register_input_processor(input_processor_for_pixtral)
325
+
326
+@MULTIMODAL_REGISTRY.register_processor(PixtralMultiModalProcessor,
327
+ info=PixtralProcessingInfo,
328
+ dummy_inputs=PixtralDummyInputsBuilder)
329
class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
330
SupportsPP):
331
@@ -191,13 +343,6 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
343
if key in dataclass_fields
344
}
345
194
- if not ("image_break_token_id" in vision_args
195
- and "image_end_token_id" in vision_args):
196
- raise ValueError(
197
- "'image_break_token_id' and 'image_end_token_id' not found "
198
- "in the vision_encoder arguments. Please download the latest "
199
- "version of 'params.json' from the model repository.")
200
-
346
self.vision_args = VisionEncoderArgs(**vision_args)
347
348
# init MistralForCausalLM
@@ -221,36 +366,92 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
366
367
return get_sampler()
368
369
+ def _parse_and_validate_image_input(
370
+ self, **kwargs: object) -> Optional[PixtralImagePixelInputs]:
371
+ images = kwargs.pop("images", None)
372
+ if images is None:
373
+ return None
374
+
375
+ if not isinstance(images, (torch.Tensor, list)):
376
+ raise ValueError("Incorrect type of images. "
377
+ f"Got type: {type(images)}")
378
+
379
+ embed_is_patch = kwargs.pop("embed_is_patch")
380
+ if not isinstance(embed_is_patch, (torch.Tensor, list)):
381
+ raise ValueError("Incorrect type of embed_is_patch. "
382
+ f"Got type: {type(embed_is_patch)}")
383
+
384
+ num_patches = kwargs.pop("num_patches")
385
+ if not isinstance(num_patches, (torch.Tensor, list)):
386
+ raise ValueError("Incorrect type of num_patches. "
387
+ f"Got type: {type(num_patches)}")
388
+
389
+ return PixtralImagePixelInputs(
390
+ type="pixel_values",
391
+ images=flatten_bn(images),
392
+ embed_is_patch=embed_is_patch,
393
+ num_patches=num_patches,
394
+ )
395
+
396
+ def _process_image_input(
397
+ self,
398
+ image_input: PixtralImagePixelInputs,
399
+ ) -> tuple[torch.Tensor, ...]:
400
+ images = image_input["images"]
401
+
402
+ image_features = self.vision_encoder(images)
403
+ feature_sizes = [
404
+ image_feature.shape[0] for image_feature in image_features
405
+ ]
406
+
407
+ image_embeds = self.vision_language_adapter(torch.cat(image_features))
408
+ image_embeds = torch.split(image_embeds, feature_sizes)
409
+ return image_embeds
410
+
411
+ def _get_mm_embeds(
412
+ self,
413
+ features: torch.Tensor, # Shape: (num_patch, d)
414
+ num_patches: torch.Tensor, # Shape: (num_images,)
415
+ embed_is_patch: torch.Tensor, # Shape: (num_images, num_embeds)
416
+ ) -> tuple[torch.Tensor, ...]:
417
+ """Scatter the patch features into a contiguous tensor that corresponds
418
+ to the embedding tokens defined by the multimodal processor.
419
+
420
+ Mostly copied from `Molmo._get_mm_embeds`. See following fixme comment.
421
+ """
422
+ # Insert columns of nan values according to `embed_is_patch`. This work
423
+ # ideally should be done in `_process_image_input`, but
424
+ # `_process_image_input` is used in both V0 and V1 path. It's safer to
425
+ # put the logic here.
426
+ # FIXME: Move this logic to `_process_image_input` when v0 is
427
+ # deprecated. Merge this function with `Molmo._get_mm_embeds`.
428
+ num_patches_per_image: list[int] = num_patches.tolist()
429
+
430
+ embeds_flat = features.new_full(
431
+ (sum(num_patches_per_image), *features.shape[1:]),
432
+ fill_value=torch.nan,
433
+ )
434
+ embeds_flat[embed_is_patch.view(-1)] = features
435
+
436
+ return embeds_flat.split(num_patches_per_image)
437
+
438
def get_multimodal_embeddings(
439
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
226
- image_input, image_tokens = self._parse_and_validate_image_input(
227
- **kwargs)
440
+ image_input = self._parse_and_validate_image_input(**kwargs)
441
if image_input is None:
442
return None
443
231
- vision_embeddings = self._process_image_input(image_input)
232
-
233
- # NOTE: We patch the outputs of the vision encoder with embeddings
234
- # from `[IMG_BREAK]` and `[IMG_END]` tokens.
235
- image_embeds = self.language_model.get_input_embeddings(image_tokens)
236
- image_token_mask = image_tokens == self.vision_args.image_token_id
237
- image_embeds[image_token_mask] = vision_embeddings
444
+ image_features = self._process_image_input(image_input)
445
239
- # NOTE: Image embeddings are split into separate tensors for each image
240
- # by the indices of `[IMG_END]` token.
241
- image_end_mask = image_tokens == self.vision_args.image_end_token_id
242
- split_indices = torch.where(image_end_mask)[0] + 1
243
- if len(split_indices) <= 1:
244
- # Do not split, return as tensor of shape [1, fs, hs]
245
- return image_embeds.unsqueeze(0)
446
+ if kwargs.get("v0_path", False):
447
+ return image_features
448
247
- # If the last split index is the last index in image_tokens, we
248
- # ignore it to avoid empty split tensor
249
- if split_indices[-1] == len(image_tokens):
250
- split_indices = split_indices[:-1]
251
-
252
- image_embeds = image_embeds.tensor_split(split_indices.cpu())
253
- return image_embeds
449
+ return flatten_2d_lists(
450
+ self._get_mm_embeds(*args) for args in zip(
451
+ image_features,
452
+ image_input["num_patches"],
453
+ image_input["embed_is_patch"],
454
+ ))
455
456
def get_input_embeddings(
457
self,
@@ -259,12 +460,17 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
460
) -> torch.Tensor:
461
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
462
if multimodal_embeddings is not None:
463
+ # Extract the patch tokens
464
+ patch_embeddings = json_map_leaves(
465
+ lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
466
+ cast(JSONTree[torch.Tensor], multimodal_embeddings),
467
+ )
468
inputs_embeds = merge_multimodal_embeddings(
263
- input_ids, inputs_embeds, multimodal_embeddings, [
264
- self.vision_args.image_token_id,
265
- self.vision_args.image_break_token_id,
266
- self.vision_args.image_end_token_id,
267
- ])
469
+ input_ids,
470
+ inputs_embeds,
471
+ cast(NestedTensors, patch_embeddings),
472
+ self.vision_args.image_token_id,
473
+ )
474
return inputs_embeds
475
476
def forward(
@@ -275,14 +481,14 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
481
inputs_embeds: Optional[torch.Tensor] = None,
482
**kwargs: object,
483
) -> Union[torch.Tensor, IntermediateTensors]:
278
- """Run forward pass for pixtral.
279
- """
484
+ """Run forward pass for pixtral."""
485
if intermediate_tensors is not None:
486
inputs_embeds = None
487
488
# NOTE: In v1, inputs_embeds is always generated at model runner, this
489
# condition is for v0 compatibility.
490
elif inputs_embeds is None:
491
+ kwargs.update({"v0_path": True})
492
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
493
inputs_embeds = self.get_input_embeddings(input_ids,
494
vision_embeddings)
@@ -295,47 +501,6 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
501
502
return hidden_states
503
298
- def _parse_and_validate_image_input(
299
- self,
300
- images: Optional[Union[List[List[torch.Tensor]], List[torch.Tensor],
301
- torch.Tensor]] = None,
302
- image_tokens: Optional[torch.Tensor] = None,
303
- ) -> Tuple[Optional[List[torch.Tensor]], Optional[torch.Tensor]]:
304
- if images is None:
305
- return None, None
306
-
307
- if isinstance(images, torch.Tensor):
308
- # if passed as batch take all images
309
- N, B, C, W, H = images.shape
310
- images = images.reshape(N * B, C, W, H)
311
- images = [images[i] for i in range(images.size(0))]
312
- elif isinstance(images, list):
313
- # if passed as list flatten lists of tensors
314
- flatten_images = []
315
- for imgs_per_req in images:
316
- imgs_per_req = [
317
- imgs_per_req[i] for i in range(imgs_per_req.size(0))
318
- ] if isinstance(imgs_per_req, torch.Tensor) else imgs_per_req
319
-
320
- flatten_images.extend(imgs_per_req)
321
-
322
- images = flatten_images
323
-
324
- if isinstance(image_tokens, torch.Tensor):
325
- # image_tokens are batched
326
- image_tokens = image_tokens.flatten()
327
- elif isinstance(image_tokens, list):
328
- # image_tokens are of different lengths thus passed as a list
329
- image_tokens = torch.cat(image_tokens)
330
-
331
- assert image_tokens.dim() == 1
332
-
333
- return images, image_tokens
334
-
335
- def _process_image_input(self,
336
- image_input: List[torch.Tensor]) -> torch.Tensor:
337
- return self.vision_language_adapter(self.vision_encoder(image_input))
338
-
504
def compute_logits(
505
self,
506
hidden_states: torch.Tensor,
@@ -400,8 +565,6 @@ class VisionEncoderArgs:
565
num_attention_heads: int
566
rope_theta: float # for rope-2D
567
image_token_id: int
403
- image_break_token_id: int
404
- image_end_token_id: int
568
adapter_bias: bool = True
569
570
@@ -637,9 +800,13 @@ class VisionTransformer(nn.Module):
800
self.patch_conv(img.unsqueeze(0).to(self.dtype)) for img in images
801
]
802
803
+ patch_embeds = [
804
+ p.flatten(2).permute(0, 2, 1) for p in patch_embeds_list
805
+ ]
806
+ embed_sizes = [p.shape[1] for p in patch_embeds]
807
+
808
# flatten to a single sequence
641
- patch_embeds = torch.cat(
642
- [p.flatten(2).permute(0, 2, 1) for p in patch_embeds_list], dim=1)
809
+ patch_embeds = torch.cat(patch_embeds, dim=1)
810
patch_embeds = self.ln_pre(patch_embeds)
811
812
# positional embeddings
@@ -655,8 +822,8 @@ class VisionTransformer(nn.Module):
822
"with the Mistral format")
823
out = self.transformer(patch_embeds, mask=mask, freqs_cis=freqs_cis)
824
658
- # remove batch dimension of the single sequence
659
- return out.squeeze(0)
825
+ # squeeze dim 0 and split into separate tensors for each image
826
+ return torch.split(out.squeeze(0), embed_sizes)
827
828
829
class VisionLanguageAdapter(nn.Module):
@@ -978,9 +1145,9 @@ class PixtralHFVisionModel(nn.Module):
1145
1146
def forward(
1147
self,
981
- pixel_values: List[torch.Tensor],
1148
+ pixel_values: list[torch.Tensor],
1149
feature_sample_layers: Optional[list[int]] = None,
983
- ) -> torch.Tensor:
1150
+ ) -> tuple[torch.Tensor, ...]:
1151
"""
1152
Args:
1153
pixel_values: Each image to be processed will be a separate tensor
@@ -1039,8 +1206,7 @@ class PixtralHFVisionModel(nn.Module):
1206
self.config.num_hidden_layers)
1207
1208
# squeeze dim 0 and split into separate tensors for each image
1042
- out = torch.split(torch.squeeze(out), embed_sizes)
1043
- return out
1209
+ return torch.split(out.squeeze(0), embed_sizes)
1210
1211
# (TODO) Add prefix argument for filtering out weights to be loaded
1212
# ref: https://github.com/vllm-project/vllm/pull/7186#discussion_r1734163986
vllm/multimodal/processing.py
+9
-5
@@ -77,7 +77,9 @@ class PromptIndexTargets:
77
else:
78
if isinstance(prefix, str):
79
# Make both `list[int]`
80
- prefix = encode_tokens(tokenizer, prefix)
80
+ prefix = encode_tokens(tokenizer,
81
+ prefix,
82
+ add_special_tokens=False)
83
84
match_idx = len(prefix)
85
return match_idx if prompt[:match_idx] == prefix else None
@@ -318,7 +320,7 @@ def _cached_encode(
320
tokenizer: AnyTokenizer,
321
text: str,
322
*,
321
- add_special_tokens: bool = False,
323
+ add_special_tokens: Optional[bool] = None,
324
) -> list[int]:
325
return encode_tokens(tokenizer,
326
text,
@@ -330,7 +332,7 @@ def _cached_decode(
332
tokenizer: AnyTokenizer,
333
token_ids: tuple[int, ...],
334
*,
333
- skip_special_tokens: bool = False,
335
+ skip_special_tokens: Optional[bool] = None,
336
) -> str:
337
return decode_tokens(tokenizer,
338
list(token_ids),
@@ -395,7 +397,9 @@ class _BoundPromptSequence:
397
def token_ids(self) -> list[int]:
398
if self._token_ids is None:
399
assert self._text is not None
398
- self._token_ids = _cached_encode(self.tokenizer, self._text)
400
+ self._token_ids = _cached_encode(self.tokenizer,
401
+ self._text,
402
+ add_special_tokens=False)
403
404
return self._token_ids
405
@@ -1046,7 +1050,7 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1050
mm_items: MultiModalDataItems,
1051
hf_processor_mm_kwargs: Mapping[str, object],
1052
out_mm_kwargs: MultiModalKwargs,
1049
- ) -> list[PromptUpdate]:
1053
+ ) -> Sequence[PromptUpdate]:
1054
"""
1055
Given the original multi-modal items for this modality
1056
and HF-processed data, output the updates to perform.
vllm/transformers_utils/tokenizer.py
+15
-4
@@ -34,13 +34,20 @@ def decode_tokens(
34
tokenizer: AnyTokenizer,
35
token_ids: list[int],
36
*,
37
- skip_special_tokens: bool = False,
37
+ skip_special_tokens: Optional[bool] = None,
38
) -> str:
39
"""
40
Backend-agnostic equivalent of HF's
41
- :code:`tokenizer.decode(token_ids, skip_special_tokens=...)`.
41
+ :code:`tokenizer.decode(token_ids, ...)`.
42
+
43
+ :code:`skip_special_tokens=None` means to use the backend's default
44
+ settings.
45
"""
43
- return tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)
46
+ if skip_special_tokens is not None:
47
+ return tokenizer.decode(token_ids,
48
+ skip_special_tokens=skip_special_tokens)
49
+
50
+ return tokenizer.decode(token_ids)
51
52
53
def encode_tokens(
@@ -51,10 +58,14 @@ def encode_tokens(
58
) -> list[int]:
59
"""
60
Backend-agnostic equivalent of HF's
54
- :code:`tokenizer.encode(text, add_special_tokens=...)`.
61
+ :code:`tokenizer.encode(text, ...)`.
62
+
63
+ :code:`add_special_tokens=None` means to use the backend's default
64
+ settings.
65
"""
66
if add_special_tokens is not None:
67
return tokenizer.encode(text, add_special_tokens=add_special_tokens)
68
+
69
return tokenizer.encode(text)
70
71
vllm/utils.py
+1
-1
@@ -845,7 +845,7 @@ def is_list_of(
845
assert_never(check)
846
847
848
-def flatten_2d_lists(lists: list[list[T]]) -> list[T]:
848
+def flatten_2d_lists(lists: Iterable[Iterable[T]]) -> list[T]:
849
"""Flatten a list of lists to a single list."""
850
return [item for sublist in lists for item in sublist]
851