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] Fix embedding assignment for InternVL-based models (#15086)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Mar 20, 2025 at 11:40 UTC
ffa443afedd3ffefb8dbe20607692950b78c1496
7 files changed
+123
-106
examples/offline_inference/vision_language.py
-1
@@ -169,7 +169,6 @@ def run_gemma3(questions: list[str], modality: str) -> ModelRequestData:
169
model=model_name,
170
max_model_len=2048,
171
max_num_seqs=2,
172
- # Default is False; setting it to True is not supported in V1 yet
172
mm_processor_kwargs={"do_pan_and_scan": True},
173
disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
174
)
examples/offline_inference/vision_language_multi_image.py
-2
@@ -91,8 +91,6 @@ def load_gemma3(question: str, image_urls: list[str]) -> ModelRequestData:
91
model=model_name,
92
max_model_len=8192,
93
max_num_seqs=2,
94
- # Default is False; setting it to True is not supported in V1 yet
95
- mm_processor_kwargs={"do_pan_and_scan": True},
94
limit_mm_per_prompt={"image": len(image_urls)},
95
)
96
vllm/model_executor/models/gemma3_mm.py
+1
-1
@@ -183,7 +183,7 @@ class Gemma3ProcessingInfo(BaseProcessingInfo):
183
image_width: int,
184
image_height: int,
185
processor: Optional[Gemma3Processor],
186
- ) -> PromptUpdateDetails:
186
+ ) -> PromptUpdateDetails[str]:
187
if processor is None:
188
processor = self.get_hf_processor()
189
vllm/model_executor/models/h2ovl.py
+6
-16
@@ -249,20 +249,15 @@ class H2OVLProcessor(BaseInternVLProcessor):
249
def image_token_id(self) -> int:
250
return self.tokenizer.get_vocab()[IMG_CONTEXT]
251
252
- def get_image_repl_features(
252
+ def get_image_repl(
253
self,
254
feature_size: int,
255
num_patches: Optional[int],
256
- ) -> str:
257
- return IMG_CONTEXT * feature_size
256
+ ) -> PromptUpdateDetails[str]:
257
+ repl_features = IMG_CONTEXT * feature_size
258
+ repl_full = IMG_START + repl_features + IMG_END
259
259
- def get_image_repl_full(
260
- self,
261
- feature_size: int,
262
- num_patches: Optional[int],
263
- ) -> str:
264
- features = self.get_image_repl_features(feature_size, num_patches)
265
- return IMG_START + features + IMG_END
260
+ return PromptUpdateDetails(full=repl_full, features=repl_features)
261
262
def resolve_min_max_num(
263
self,
@@ -501,12 +496,7 @@ class H2OVLMultiModalProcessor(InternVLMultiModalProcessor[H2OVLProcessingInfo]
496
if num_patches is not None:
497
assert isinstance(num_patches, int)
498
504
- return PromptUpdateDetails(
505
- full=hf_processor.get_image_repl_full(feature_size,
506
- num_patches),
507
- features=hf_processor.get_image_repl_features(
508
- feature_size, num_patches),
509
- )
499
+ return hf_processor.get_image_repl(feature_size, num_patches)
500
501
return [
502
PromptReplacement(
vllm/model_executor/models/internvl.py
+104
-69
@@ -9,14 +9,13 @@
9
from abc import ABC, abstractmethod
10
from collections.abc import Iterable, Mapping, Sequence
11
from functools import cached_property
12
-from typing import (List, Literal, Optional, Set, Tuple, TypedDict, TypeVar,
13
- Union)
12
+from typing import Literal, Optional, Set, Tuple, TypedDict, TypeVar, Union
13
14
import torch
15
import torch.nn as nn
16
import torchvision.transforms as T
17
from PIL import Image
19
-from transformers import BatchFeature, PretrainedConfig, TensorType
18
+from transformers import BatchEncoding, PretrainedConfig, TensorType
19
20
from vllm.config import VllmConfig
21
from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -36,10 +35,12 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
35
from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
36
from vllm.sequence import IntermediateTensors
37
from vllm.transformers_utils.tokenizer import AnyTokenizer
38
+from vllm.utils import flatten_2d_lists
39
40
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
41
from .utils import (AutoWeightsLoader, flatten_bn, init_vllm_registered_model,
42
maybe_prefix, merge_multimodal_embeddings)
43
+from .vision import scatter_patch_features, select_patch_features
44
45
IMG_START = '<img>'
46
IMG_END = '</img>'
@@ -51,16 +52,26 @@ IMAGENET_STD = (0.229, 0.224, 0.225)
52
53
class InternVLImagePixelInputs(TypedDict):
54
type: Literal["pixel_values"]
54
- data: torch.Tensor
55
+ pixel_values_flat: torch.Tensor
56
"""
57
Shape:
58
`(batch_size * num_images * (1 + num_patches), num_channels, height, width)`
59
"""
59
- patches_per_image: List[int]
60
+
61
+ num_patches: torch.Tensor
62
+ """Shape: `(batch_size * num_images)`"""
63
+
64
+ embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
65
"""
61
- List of number of total patches for each image in the batch.
66
+ A boolean mask indicating which image embeddings correspond
67
+ to patch tokens.
68
+
69
+ Shape: `(batch_size, num_images, num_embeds)`
70
"""
71
72
+ num_embeds: Union[torch.Tensor, list[torch.Tensor]]
73
+ """Shape: `(batch_size, num_images)`"""
74
+
75
76
class InternVLImageEmbeddingInputs(TypedDict):
77
type: Literal["image_embeds"]
@@ -286,19 +297,11 @@ class BaseInternVLProcessor(ABC):
297
raise NotImplementedError
298
299
@abstractmethod
289
- def get_image_repl_features(
300
+ def get_image_repl(
301
self,
302
feature_size: int,
303
num_patches: Optional[int],
293
- ) -> str:
294
- raise NotImplementedError
295
-
296
- @abstractmethod
297
- def get_image_repl_full(
298
- self,
299
- feature_size: int,
300
- num_patches: Optional[int],
301
- ) -> str:
304
+ ) -> PromptUpdateDetails[str]:
305
raise NotImplementedError
306
307
def resolve_min_max_num(
@@ -394,7 +397,7 @@ class BaseInternVLProcessor(ABC):
397
max_dynamic_patch: Optional[int] = None,
398
dynamic_image_size: Optional[bool] = None,
399
return_tensors: Optional[Union[str, TensorType]] = None,
397
- ) -> BatchFeature:
400
+ ) -> Mapping[str, NestedTensors]:
401
if text is None:
402
text = []
403
if not isinstance(text, list):
@@ -413,28 +416,41 @@ class BaseInternVLProcessor(ABC):
416
max_dynamic_patch=max_dynamic_patch,
417
dynamic_image_size=dynamic_image_size,
418
)
416
- image_inputs = {
417
- "pixel_values_flat": torch.cat(pixel_values_lst),
418
- "image_num_patches": list(map(len, pixel_values_lst)),
419
+ image_inputs: dict[str, NestedTensors] = {
420
+ "pixel_values_flat":
421
+ torch.cat(pixel_values_lst),
422
+ "image_num_patches":
423
+ torch.tensor([len(item) for item in pixel_values_lst]),
424
}
425
426
+ tokenizer = self.tokenizer
427
+ image_token_id = self.image_token_id
428
+
429
+ num_embeds = list[int]()
430
+ embed_is_patch = list[torch.Tensor]()
431
+
432
for pixel_values in pixel_values_lst:
433
num_patches = pixel_values.shape[0]
434
feature_size = num_patches * self.num_image_token
435
425
- image_repl = self.get_image_repl_full(feature_size,
426
- num_patches)
427
- text = [t.replace('<image>', image_repl, 1) for t in text]
436
+ image_repl = self.get_image_repl(feature_size, num_patches)
437
+ feature_tokens = tokenizer.encode(image_repl.features,
438
+ add_special_tokens=False)
439
+
440
+ text = [t.replace('<image>', image_repl.full, 1) for t in text]
441
+ num_embeds.append(len(feature_tokens))
442
+ embed_is_patch.append(
443
+ torch.tensor(feature_tokens) == image_token_id)
444
+
445
+ image_inputs["num_embeds"] = torch.tensor(num_embeds)
446
+ image_inputs["embed_is_patch"] = embed_is_patch
447
448
text_inputs = self.tokenizer(text)
449
431
- return BatchFeature(
432
- {
433
- **text_inputs,
434
- **image_inputs,
435
- },
436
- tensor_type=return_tensors,
437
- )
450
+ return {
451
+ **BatchEncoding(text_inputs, tensor_type=return_tensors),
452
+ **image_inputs,
453
+ }
454
455
456
class InternVLProcessor(BaseInternVLProcessor):
@@ -443,20 +459,15 @@ class InternVLProcessor(BaseInternVLProcessor):
459
def image_token_id(self) -> int:
460
return self.tokenizer.get_vocab()[IMG_CONTEXT]
461
446
- def get_image_repl_features(
462
+ def get_image_repl(
463
self,
464
feature_size: int,
465
num_patches: Optional[int],
450
- ) -> str:
451
- return IMG_CONTEXT * feature_size
466
+ ) -> PromptUpdateDetails[str]:
467
+ repl_features = IMG_CONTEXT * feature_size
468
+ repl_full = IMG_START + repl_features + IMG_END
469
453
- def get_image_repl_full(
454
- self,
455
- feature_size: int,
456
- num_patches: Optional[int],
457
- ) -> str:
458
- features = self.get_image_repl_features(feature_size, num_patches)
459
- return IMG_START + features + IMG_END
470
+ return PromptUpdateDetails(full=repl_full, features=repl_features)
471
472
473
class BaseInternVLProcessingInfo(BaseProcessingInfo):
@@ -566,16 +577,15 @@ class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
577
prompt: str,
578
mm_data: Mapping[str, object],
579
mm_kwargs: Mapping[str, object],
569
- ) -> BatchFeature:
580
+ ) -> Mapping[str, NestedTensors]:
581
processed_outputs = super()._call_hf_processor(
582
prompt=prompt,
583
mm_data=mm_data,
584
mm_kwargs=mm_kwargs,
585
)
586
576
- image_token_id = self.info.get_hf_processor(**mm_kwargs).image_token_id
577
- image_data = mm_data.get("images", [])
578
- assert isinstance(image_data, list)
587
+ hf_processor = self.info.get_hf_processor(**mm_kwargs)
588
+ image_token_id = hf_processor.image_token_id
589
590
# Since there may be extra tokens in the feature placeholders,
591
# we need to pass the image token ID to the model to select the
@@ -586,7 +596,7 @@ class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
596
597
def _get_mm_fields_config(
598
self,
589
- hf_inputs: BatchFeature,
599
+ hf_inputs: Mapping[str, NestedTensors],
600
hf_processor_mm_kwargs: Mapping[str, object],
601
) -> Mapping[str, MultiModalFieldConfig]:
602
image_num_patches = hf_inputs.get("image_num_patches", torch.empty(0))
@@ -596,6 +606,8 @@ class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
606
pixel_values_flat=MultiModalFieldConfig.flat_from_sizes(
607
"image", image_num_patches),
608
image_num_patches=MultiModalFieldConfig.batched("image"),
609
+ embed_is_patch=MultiModalFieldConfig.batched("image"),
610
+ num_embeds=MultiModalFieldConfig.batched("image"),
611
image_embeds=MultiModalFieldConfig.batched("image"),
612
image_token_id=MultiModalFieldConfig.shared("image", num_images),
613
)
@@ -637,12 +649,7 @@ class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
649
if num_patches is not None:
650
assert isinstance(num_patches, int)
651
640
- return PromptUpdateDetails(
641
- full=hf_processor.get_image_repl_full(feature_size,
642
- num_patches),
643
- features=hf_processor.get_image_repl_features(
644
- feature_size, num_patches),
645
- )
652
+ return hf_processor.get_image_repl(feature_size, num_patches)
653
654
return [
655
PromptReplacement(
@@ -832,6 +839,8 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
839
self, **kwargs: object) -> Optional[InternVLImageInputs]:
840
pixel_values_flat = kwargs.pop("pixel_values_flat", None)
841
image_num_patches = kwargs.pop("image_num_patches", None)
842
+ embed_is_patch = kwargs.pop("embed_is_patch", None)
843
+ num_embeds = kwargs.pop("num_embeds", None)
844
image_embeds = kwargs.pop("image_embeds", None)
845
846
if pixel_values_flat is None and image_embeds is None:
@@ -858,35 +867,47 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
867
868
if not isinstance(image_num_patches, (torch.Tensor, list)):
869
raise ValueError("Incorrect type of image_num_patches. "
861
- f"Got type: {type(pixel_values_flat)}")
870
+ f"Got type: {type(image_num_patches)}")
871
+
872
+ if not isinstance(embed_is_patch, (torch.Tensor, list)):
873
+ raise ValueError("Incorrect type of embed_is_patch. "
874
+ f"Got type: {type(embed_is_patch)}")
875
+
876
+ if not isinstance(num_embeds, (torch.Tensor, list)):
877
+ raise ValueError("Incorrect type of num_embeds. "
878
+ f"Got type: {type(num_embeds)}")
879
+
880
+ pixel_values_flat = flatten_bn(pixel_values_flat, concat=True)
881
+ image_num_patches = flatten_bn(image_num_patches, concat=True)
882
883
return InternVLImagePixelInputs(
884
type="pixel_values",
865
- data=self._validate_pixel_values(
866
- flatten_bn(pixel_values_flat, concat=True)),
867
- patches_per_image=flatten_bn(image_num_patches,
868
- concat=True).tolist())
885
+ pixel_values_flat=self._validate_pixel_values(
886
+ pixel_values_flat),
887
+ num_patches=image_num_patches,
888
+ embed_is_patch=embed_is_patch,
889
+ num_embeds=num_embeds,
890
+ )
891
892
raise AssertionError("This line should be unreachable.")
893
894
def _process_image_input(
895
self,
896
image_input: InternVLImageInputs,
875
- ) -> tuple[torch.Tensor, ...]:
897
+ ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
898
if image_input["type"] == "image_embeds":
899
return image_input["data"]
900
901
assert self.vision_model is not None
902
881
- image_embeds = self.extract_feature(image_input["data"])
903
+ image_embeds = self.extract_feature(image_input["pixel_values_flat"])
904
883
- patches_per_image = image_input["patches_per_image"]
905
+ num_patches = image_input["num_patches"]
906
907
# Only one image in the current batch
886
- if len(patches_per_image) == 1:
887
- image_embeds = image_embeds.view(
908
+ if len(num_patches) == 1:
909
+ return image_embeds.view(
910
-1, self.config.text_config.hidden_size).unsqueeze(0)
889
- return image_embeds
911
912
# NOTE: Image embeddings are split into separate tensors for each image
913
# by the size of each embedding.
@@ -894,10 +915,9 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
915
image_embeds = image_embeds.view(-1,
916
self.config.text_config.hidden_size)
917
image_feature_sizes = [
897
- num_patches * feature_size for num_patches in patches_per_image
918
+ num_patches * feature_size for num_patches in num_patches
919
]
899
- image_embeds = image_embeds.split(image_feature_sizes)
900
- return image_embeds
920
+ return image_embeds.split(image_feature_sizes)
921
922
def _set_visual_token_mask(self, input_ids: torch.Tensor) -> None:
923
if self.is_mono:
@@ -911,8 +931,19 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
931
image_input = self._parse_and_validate_image_input(**kwargs)
932
if image_input is None:
933
return None
914
- vision_embeddings = self._process_image_input(image_input)
915
- return vision_embeddings
934
+
935
+ image_features = self._process_image_input(image_input)
936
+
937
+ if (kwargs.get("v0_path", False)
938
+ or image_input["type"] != "pixel_values"):
939
+ return image_features
940
+
941
+ return flatten_2d_lists(
942
+ scatter_patch_features(*args) for args in zip(
943
+ image_features,
944
+ image_input["num_embeds"],
945
+ image_input["embed_is_patch"],
946
+ ))
947
948
def get_input_embeddings(
949
self,
@@ -924,8 +955,11 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
955
assert self.img_context_token_id is not None
956
self._set_visual_token_mask(input_ids)
957
inputs_embeds = merge_multimodal_embeddings(
927
- input_ids, inputs_embeds, multimodal_embeddings,
928
- self.img_context_token_id)
958
+ input_ids,
959
+ inputs_embeds,
960
+ select_patch_features(multimodal_embeddings),
961
+ self.img_context_token_id,
962
+ )
963
return inputs_embeds
964
965
def forward(
@@ -944,6 +978,7 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
978
# NOTE: In v1, inputs_embeds is always generated at model runner, this
979
# condition is for v0 compatibility.
980
elif inputs_embeds is None:
981
+ kwargs.update({"v0_path": True})
982
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
983
inputs_embeds = self.get_input_embeddings(input_ids,
984
vision_embeddings)
vllm/model_executor/models/nvlm_d.py
+8
-13
@@ -36,11 +36,11 @@ class NVLMProcessor(BaseInternVLProcessor):
36
def image_token_id(self) -> int:
37
return self.tokenizer.get_vocab()[IMG_PAD]
38
39
- def get_image_repl_features(
39
+ def get_image_repl(
40
self,
41
feature_size: int,
42
num_patches: Optional[int],
43
- ) -> str:
43
+ ) -> PromptUpdateDetails[str]:
44
if num_patches is None:
45
raise NotImplementedError("Embedding inputs are not supported")
46
@@ -55,14 +55,9 @@ class NVLMProcessor(BaseInternVLProcessor):
55
# We include the start and end as well because "<Image><tile" is
56
# tokenized as ["<Image", "><", "tile"], resulting in assertion error
57
# when trying to find "<tile" as a subsequence of "<Image><tile"
58
- return "<Image>" + features + "</Image>"
58
+ repl = "<Image>" + features + "</Image>"
59
60
- def get_image_repl_full(
61
- self,
62
- feature_size: int,
63
- num_patches: Optional[int],
64
- ) -> str:
65
- return self.get_image_repl_features(feature_size, num_patches)
60
+ return PromptUpdateDetails(full=repl, features=repl)
61
62
63
class NVLMProcessingInfo(BaseInternVLProcessingInfo):
@@ -180,11 +175,11 @@ class NVLMMultiModalProcessor(InternVLMultiModalProcessor[NVLMProcessingInfo]):
175
if num_patches is not None:
176
assert isinstance(num_patches, int)
177
178
+ repl = hf_processor.get_image_repl(feature_size, num_patches)
179
+
180
return PromptUpdateDetails(
184
- full=hf_processor.get_image_repl_full(feature_size,
185
- num_patches) + "\n",
186
- features=hf_processor.get_image_repl_features(
187
- feature_size, num_patches) + "\n",
181
+ full=repl.full + "\n",
182
+ features=repl.features + "\n",
183
)
184
185
# See note in dummy data regarding why we have the extra newline
vllm/multimodal/processing.py
+4
-4
@@ -103,13 +103,13 @@ The token sequence or text to update.
103
104
105
@dataclass
106
-class PromptUpdateDetails:
106
+class PromptUpdateDetails(Generic[_S]):
107
"""Details about the token sequence or text that are part of the update."""
108
109
- full: PromptSeq
109
+ full: _S
110
"""The full content."""
111
112
- features: PromptSeq
112
+ features: _S
113
"""
114
The part of the content that corresponds to feature placeholders;
115
this will be replaced by the output of the vision encoder during model
@@ -117,7 +117,7 @@ class PromptUpdateDetails:
117
"""
118
119
@staticmethod
120
- def from_seq(seq: PromptSeq) -> "PromptUpdateDetails":
120
+ def from_seq(seq: _S) -> "PromptUpdateDetails[_S]":
121
return PromptUpdateDetails(full=seq, features=seq)
122
123