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, 17:15 UTC
[Misc] Clean up `scatter_patch_features` (#15559)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Mar 27, 2025 at 15:45 UTC
e6c9053f9ec0b41e9af41def67537a4a3097eeb5
6 files changed
+84
-138
vllm/model_executor/models/gemma3_mm.py
+6
-11
@@ -30,7 +30,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
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
33
34
from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
35
SupportsMultiModal, SupportsPP)
@@ -60,7 +59,7 @@ class Gemma3ImagePixelInputs(TypedDict):
59
A boolean mask indicating which image embeddings correspond
60
to patch tokens.
61
63
- Shape: `(batch_size, num_images, num_embeds)`
62
+ Shape: `(batch_size * num_images, num_embeds)`
63
"""
64
65
@@ -593,6 +592,7 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
592
593
pixel_values = flatten_bn(pixel_values, concat=True)
594
num_crops = flatten_bn(num_crops, concat=True)
595
+ embed_is_patch = flatten_bn(embed_is_patch)
596
597
return Gemma3ImagePixelInputs(
598
type="pixel_values",
@@ -635,14 +635,10 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
635
636
image_features = self._process_image_input(image_input)
637
638
- if kwargs.get("v0_path", False):
639
- return image_features
640
-
641
- return flatten_2d_lists(
642
- scatter_patch_features(*args) for args in zip(
643
- image_features,
644
- image_input["embed_is_patch"],
645
- ))
638
+ return scatter_patch_features(
639
+ image_features,
640
+ image_input["embed_is_patch"],
641
+ )
642
643
def get_input_embeddings(
644
self,
@@ -671,7 +667,6 @@ class Gemma3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP,
667
# NOTE: In v1, inputs_embeds is always generated at model runner, this
668
# condition is for v0 compatibility.
669
elif inputs_embeds is None:
674
- kwargs.update({"v0_path": True})
670
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
671
672
inputs_embeds = self.get_input_embeddings(input_ids,
vllm/model_executor/models/internvl.py
+9
-12
@@ -35,7 +35,6 @@ 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
38
39
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
40
from .utils import (AutoWeightsLoader, flatten_bn, init_vllm_registered_model,
@@ -66,13 +65,13 @@ class InternVLImagePixelInputs(TypedDict):
65
A boolean mask indicating which image embeddings correspond
66
to patch tokens.
67
69
- Shape: `(batch_size, num_images, num_embeds)`
68
+ Shape: `(batch_size * num_images, num_embeds)`
69
"""
70
71
72
class InternVLImageEmbeddingInputs(TypedDict):
73
type: Literal["image_embeds"]
75
- data: NestedTensors
74
+ data: Union[torch.Tensor, list[torch.Tensor]]
75
"""
76
A tensor of shape `(num_images, total_image_feature_size, hidden_size)`
77
or a list of tensors of shape `(total_image_feature_size, hidden_size)`
@@ -867,6 +866,7 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
866
867
pixel_values_flat = flatten_bn(pixel_values_flat, concat=True)
868
image_num_patches = flatten_bn(image_num_patches, concat=True)
869
+ embed_is_patch = flatten_bn(embed_is_patch)
870
871
return InternVLImagePixelInputs(
872
type="pixel_values",
@@ -881,7 +881,7 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
881
def _process_image_input(
882
self,
883
image_input: InternVLImageInputs,
884
- ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
884
+ ) -> Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor, ...]]:
885
if image_input["type"] == "image_embeds":
886
return image_input["data"]
887
@@ -921,15 +921,13 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
921
922
image_features = self._process_image_input(image_input)
923
924
- if (kwargs.get("v0_path", False)
925
- or image_input["type"] != "pixel_values"):
924
+ if image_input["type"] != "pixel_values":
925
return image_features
926
928
- return flatten_2d_lists(
929
- scatter_patch_features(*args) for args in zip(
930
- image_features,
931
- image_input["embed_is_patch"],
932
- ))
927
+ return scatter_patch_features(
928
+ image_features,
929
+ image_input["embed_is_patch"],
930
+ )
931
932
def get_input_embeddings(
933
self,
@@ -964,7 +962,6 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
962
# NOTE: In v1, inputs_embeds is always generated at model runner, this
963
# condition is for v0 compatibility.
964
elif inputs_embeds is None:
967
- kwargs.update({"v0_path": True})
965
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
966
inputs_embeds = self.get_input_embeddings(input_ids,
967
vision_embeddings)
vllm/model_executor/models/llava.py
+10
-12
@@ -35,7 +35,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
35
PromptReplacement, PromptUpdate)
36
from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
37
from vllm.sequence import IntermediateTensors
38
-from vllm.utils import flatten_2d_lists
38
39
from .clip import CLIPVisionModel
40
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
@@ -73,7 +72,7 @@ class PixtralHFImagePixelInputs(TypedDict):
72
A boolean mask indicating which image embeddings correspond
73
to patch tokens.
74
76
- Shape: `(batch_size, num_images, num_embeds)`
75
+ Shape: `(batch_size * num_images, num_embeds)`
76
"""
77
78
@@ -618,6 +617,8 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
617
raise ValueError("Incorrect type of embed_is_patch. "
618
f"Got type: {type(embed_is_patch)}")
619
620
+ embed_is_patch = flatten_bn(embed_is_patch)
621
+
622
return PixtralHFImagePixelInputs(
623
type="pixel_values_pixtral",
624
pixel_values=flatten_bn(pixel_values),
@@ -713,18 +714,16 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
714
if image_input is None:
715
return None
716
716
- vision_embeddings = self._process_image_input(image_input)
717
+ image_features = self._process_image_input(image_input)
718
718
- if (kwargs.get("v0_path", False)
719
- or image_input["type"] != "pixel_values_pixtral"):
719
+ if image_input["type"] != "pixel_values_pixtral":
720
# The path is used for pixtral (V0 only) and llava (V0/V1)
721
- return vision_embeddings
721
+ return image_features
722
723
- return flatten_2d_lists(
724
- scatter_patch_features(*args) for args in zip(
725
- vision_embeddings,
726
- image_input["embed_is_patch"],
727
- ))
723
+ return scatter_patch_features(
724
+ image_features,
725
+ image_input["embed_is_patch"],
726
+ )
727
728
def get_input_embeddings(
729
self,
@@ -790,7 +789,6 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
789
# NOTE: In v1, inputs_embeds is always generated at model runner, this
790
# condition is for v0 compatibility.
791
elif inputs_embeds is None:
793
- kwargs.update({"v0_path": True})
792
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
793
inputs_embeds = self.get_input_embeddings(input_ids,
794
vision_embeddings)
vllm/model_executor/models/molmo.py
+32
-73
@@ -49,7 +49,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
49
PromptInsertion, PromptUpdate)
50
from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
51
from vllm.sequence import IntermediateTensors
52
-from vllm.utils import flatten_2d_lists
52
53
from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
54
SupportsMultiModal, SupportsPP, SupportsQuant)
@@ -72,17 +71,17 @@ POOLING_SIZE = 2
71
72
class MolmoImageInputs(TypedDict):
73
images: Union[torch.Tensor, list[torch.Tensor]]
75
- """Shape: `(batch_size, num_crops, num_patch, patch_dim)`"""
74
+ """Shape: `(batch_size * num_images, num_crops, num_patch, patch_dim)`"""
75
76
image_masks: Optional[Union[torch.Tensor, list[torch.Tensor]]]
78
- """Shape: `(batch_size, num_crops, num_patch)`"""
77
+ """Shape: `(batch_size * num_images, num_crops, num_patch)`"""
78
79
feat_is_patch: Union[torch.Tensor, list[torch.Tensor]]
80
"""
81
A boolean mask indicating which image features correspond
82
to patch tokens.
83
85
- Shape: `(batch_size, num_crops, num_patch)`
84
+ Shape: `(batch_size * num_images, num_crops, num_patch)`
85
"""
86
87
embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
@@ -90,7 +89,7 @@ class MolmoImageInputs(TypedDict):
89
A boolean mask indicating which image embeddings correspond
90
to patch tokens.
91
93
- Shape: `(batch_size, num_embeds)`
92
+ Shape: `(batch_size * num_images, num_embeds)`
93
"""
94
95
num_crops: Union[torch.Tensor, list[torch.Tensor]]
@@ -696,9 +695,10 @@ class MolmoVisionBackbone(nn.Module, SupportsQuant):
695
return image_features
696
697
def forward(
699
- self, images: torch.Tensor, image_masks: torch.Tensor
700
- ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
701
-
698
+ self,
699
+ images: torch.Tensor,
700
+ image_masks: torch.Tensor,
701
+ ) -> torch.Tensor:
702
# image_features: (batch_size, num_crops(=num_image), num_patch, nximage_emb_dim) # noqa: E501
703
batch_size, num_image = images.shape[:2]
704
images = images.to(device=self.device, dtype=self.dtype)
@@ -1491,6 +1491,8 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1491
f"Got type: {type(img_patch_id)}")
1492
self.img_patch_id = img_patch_id.flatten().unique().item()
1493
1494
+ embed_is_patch = flatten_bn(embed_is_patch)
1495
+
1496
return MolmoImageInputs(
1497
images=images,
1498
image_masks=image_masks,
@@ -1502,13 +1504,17 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1504
def _process_image_input(
1505
self,
1506
image_input: MolmoImageInputs,
1505
- ) -> Union[torch.Tensor, list[torch.Tensor]]:
1506
- if isinstance(image_input["images"], list):
1507
+ ) -> list[torch.Tensor]:
1508
+ images = image_input["images"]
1509
+ image_masks = image_input["image_masks"]
1510
+ feat_is_patch = image_input["feat_is_patch"]
1511
+ num_crops = image_input["num_crops"]
1512
+
1513
+ if isinstance(images, list):
1514
# Call the vision backbone on the whole batch at once
1508
- images_flat = flatten_bn(image_input["images"], concat=True)
1509
- image_masks_flat = (None if (image_masks :=
1510
- image_input["image_masks"]) is None
1511
- else flatten_bn(image_masks, concat=True))
1515
+ images_flat = flatten_bn(images, concat=True)
1516
+ image_masks_flat = (None if image_masks is None else flatten_bn(
1517
+ image_masks, concat=True))
1518
1519
image_features_flat = self.vision_backbone(
1520
images=images_flat.unsqueeze(0),
@@ -1517,63 +1523,19 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1523
).squeeze(0)
1524
1525
# Reconstruct the batch dimension
1520
- image_features = image_features_flat.split(
1521
- image_input["num_crops"].sum(-1).tolist())
1526
+ num_crops_per_image = [nc.sum().item() for nc in num_crops]
1527
+ image_features = image_features_flat.split(num_crops_per_image)
1528
else:
1529
image_features = self.vision_backbone(
1524
- images=image_input["images"],
1525
- image_masks=image_input["image_masks"],
1530
+ images=images,
1531
+ image_masks=image_masks,
1532
)
1533
1528
- return image_features
1529
-
1530
- def _get_mm_embeds(
1531
- self,
1532
- features: torch.Tensor, # Shape: (num_crop, num_patch, d)
1533
- feat_is_patch: torch.Tensor, # Shape: (num_crop, num_patch)
1534
- num_crops: torch.Tensor, # Shape: (num_images,)
1535
- embed_is_patch: torch.Tensor, # Shape: (num_embeds,)
1536
- ) -> tuple[torch.Tensor, ...]:
1537
- """
1538
- Scatter the patch features into a contiguous tensor that corresponds
1539
- to the embedding tokens defined by the multimodal processor.
1540
-
1541
- Note:
1542
- The original code only considers patch tokens as feature
1543
- tokens, but our processor considers all image-related tokens
1544
- as feature tokens because the feature tokens need to be
1545
- consecutive in `input_ids`.
1546
-
1547
- Example:
1548
- A simplified example for one item in the batch:
1549
-
1550
- .. code-block::
1551
-
1552
- Embedding tokens (from HF processor):
1553
- [<start> <patch> <patch> <col> <patch> <patch> <col> <end> ]
1554
-
1555
- embed_is_patch (from HF processor):
1556
- [ False True True False True True False False ]
1557
-
1558
- Encoder outputs (from model):
1559
- [ p1 p2 0 p3 p4 0 ]
1560
-
1561
- feat_is_patch (from HF processor):
1562
- [ True True False True True False ]
1563
-
1564
- The resulting embedding tensor is:
1565
- [ nan p1 p2 nan p3 p4 nan nan ]
1566
- """
1567
- num_crops_per_image = num_crops.tolist()
1568
- feats_per_image = features.split(num_crops_per_image)
1569
- f_is_patch_per_image = feat_is_patch.split(num_crops_per_image)
1570
-
1571
- features = torch.cat([
1534
+ # Only the features corresponding to patch tokens are relevant
1535
+ return [
1536
feats[f_is_patch]
1573
- for feats, f_is_patch in zip(feats_per_image, f_is_patch_per_image)
1574
- ])
1575
-
1576
- return scatter_patch_features(features, embed_is_patch)
1537
+ for feats, f_is_patch in zip(image_features, feat_is_patch)
1538
+ ]
1539
1540
def get_multimodal_embeddings(
1541
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
@@ -1583,13 +1545,10 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1545
1546
image_features = self._process_image_input(image_input)
1547
1586
- return flatten_2d_lists(
1587
- self._get_mm_embeds(*args) for args in zip(
1588
- image_features,
1589
- image_input["feat_is_patch"],
1590
- image_input["num_crops"],
1591
- image_input["embed_is_patch"],
1592
- ))
1548
+ return scatter_patch_features(
1549
+ image_features,
1550
+ image_input["embed_is_patch"],
1551
+ )
1552
1553
def get_input_embeddings(
1554
self,
vllm/model_executor/models/pixtral.py
+7
-11
@@ -42,7 +42,6 @@ from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
42
from vllm.sequence import IntermediateTensors
43
from vllm.transformers_utils.tokenizer import (MistralTokenizer,
44
cached_tokenizer_from_config)
45
-from vllm.utils import flatten_2d_lists
45
46
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
47
from .utils import (flatten_bn, init_vllm_registered_model, maybe_prefix,
@@ -74,7 +73,7 @@ class PixtralImagePixelInputs(TypedDict):
73
A boolean mask indicating which image embeddings correspond
74
to patch tokens.
75
77
- Shape: `(batch_size, num_images, num_embeds)`
76
+ Shape: `(batch_size * num_images, num_embeds)`
77
"""
78
79
@@ -387,6 +386,8 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
386
raise ValueError("Incorrect type of embed_is_patch. "
387
f"Got type: {type(embed_is_patch)}")
388
389
+ embed_is_patch = flatten_bn(embed_is_patch)
390
+
391
return PixtralImagePixelInputs(
392
type="pixel_values",
393
images=flatten_bn(images),
@@ -428,14 +429,10 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
429
430
image_features = self._process_image_input(image_input)
431
431
- if kwargs.get("v0_path", False):
432
- return image_features
433
-
434
- return flatten_2d_lists(
435
- scatter_patch_features(*args) for args in zip(
436
- image_features,
437
- image_input["embed_is_patch"],
438
- ))
432
+ return scatter_patch_features(
433
+ image_features,
434
+ image_input["embed_is_patch"],
435
+ )
436
437
def get_input_embeddings(
438
self,
@@ -467,7 +464,6 @@ class PixtralForConditionalGeneration(nn.Module, SupportsMultiModal,
464
# NOTE: In v1, inputs_embeds is always generated at model runner, this
465
# condition is for v0 compatibility.
466
elif inputs_embeds is None:
470
- kwargs.update({"v0_path": True})
467
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
468
inputs_embeds = self.get_input_embeddings(input_ids,
469
vision_embeddings)
vllm/model_executor/models/vision.py
+20
-19
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
from abc import ABC, abstractmethod
4
+from collections.abc import Sequence
5
from typing import Final, Generic, Optional, Protocol, TypeVar, Union, cast
6
7
import torch
@@ -154,8 +155,8 @@ def resolve_visual_encoder_outputs(
155
156
157
def scatter_patch_features(
157
- features: torch.Tensor,
158
- embed_is_patch: Union[torch.Tensor, list[torch.Tensor]],
158
+ patches: Union[torch.Tensor, Sequence[torch.Tensor]],
159
+ embed_is_patch: Union[torch.Tensor, Sequence[torch.Tensor]],
160
) -> tuple[torch.Tensor, ...]:
161
"""
162
Scatter the patch features into a contiguous tensor that corresponds
@@ -165,8 +166,8 @@ def scatter_patch_features(
166
can be filtered out by :func`select_patch_features`.
167
168
Args:
168
- features: The patch features, concatenated across each image.
169
- Shape: `(num_patch, feature_depth)`
169
+ patches: The patch features for each image.
170
+ Shape: `(num_images, <patch_dims>, feature_depth)`
171
embed_is_patch: A boolean mask indicating which image embeddings
172
correspond to patch tokens for each image.
173
Shape: `(num_images, num_embeds)`
@@ -194,21 +195,21 @@ def scatter_patch_features(
195
The resulting embedding tensor is:
196
[ nan p1 p2 nan p3 p4 nan nan ]
197
"""
197
- num_embeds_per_image = [
198
- e_is_patch.numel() for e_is_patch in embed_is_patch
199
- ]
200
- if isinstance(embed_is_patch, torch.Tensor):
201
- embed_is_patch_flat = embed_is_patch.view(-1)
202
- else:
203
- embed_is_patch_flat = torch.cat(embed_is_patch)
204
-
205
- embeds_flat = features.new_full(
206
- (sum(num_embeds_per_image), features.shape[-1]),
207
- fill_value=torch.nan,
208
- )
209
- embeds_flat[embed_is_patch_flat] = features.flatten(0, -2)
210
-
211
- return embeds_flat.split(num_embeds_per_image)
198
+ if len(patches) != len(embed_is_patch):
199
+ raise ValueError(f"Inconsistent num_images: {len(patches)=} vs. "
200
+ f"{len(embed_is_patch)=}")
201
+
202
+ def get_embed_one(patches_one: torch.Tensor, e_is_patch: torch.Tensor):
203
+ embed_one = patches_one.new_full(
204
+ (e_is_patch.shape[0], patches_one.shape[-1]),
205
+ fill_value=torch.nan,
206
+ )
207
+ embed_one[e_is_patch] = patches_one.flatten(0, -2)
208
+ return embed_one
209
+
210
+ return tuple(
211
+ get_embed_one(patches_one, e_is_patch)
212
+ for patches_one, e_is_patch in zip(patches, embed_is_patch))
213
214
215
def select_patch_features(