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, 21:00 UTC
[V1][VLM][Pixtral-HF] Support Pixtral-HF on V1 (#14275)
Signed-off-by: Linkun Chen <github@lkchen.net>
lkchen committed
Mar 6, 2025 at 00:58 UTC
5d802522a783ba31a150df80499a71acabd8009c
4 files changed
+175
-16
docs/source/models/supported_models.md
+1
-5
@@ -866,7 +866,7 @@ See [this page](#generative-models) for more information on how to use generativ
866
- * `PixtralForConditionalGeneration`
867
* Pixtral
868
* T + I<sup>+</sup>
869
- * `mistralai/Pixtral-12B-2409`, `mistral-community/pixtral-12b` (see note), etc.
869
+ * `mistralai/Pixtral-12B-2409`, `mistral-community/pixtral-12b`, etc.
870
*
871
* ✅︎
872
* ✅︎
@@ -930,10 +930,6 @@ For more details, please see: <gh-pr:4087#issuecomment-2250397630>
930
Currently the PaliGemma model series is implemented without PrefixLM attention mask. This model series may be deprecated in a future release.
931
:::
932
933
-:::{note}
934
-`mistral-community/pixtral-12b` does not support V1 yet.
935
-:::
936
-
933
:::{note}
934
To use Qwen2.5-VL series models, you have to install Hugging Face Transformers library from source via `pip install git+https://github.com/huggingface/transformers`.
935
:::
vllm/model_executor/models/llava.py
+164
-7
@@ -4,7 +4,7 @@ from abc import abstractmethod
4
from collections.abc import Iterable, Mapping, Sequence
5
from functools import cached_property
6
from typing import (Final, List, Literal, Optional, Protocol, Set, Tuple,
7
- TypedDict, TypeVar, Union)
7
+ TypedDict, TypeVar, Union, cast)
8
9
import torch
10
import torch.nn as nn
@@ -35,6 +35,7 @@ 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 JSONTree, flatten_2d_lists, json_map_leaves
39
40
from .clip import CLIPVisionModel
41
from .interfaces import SupportsMultiModal, SupportsPP
@@ -56,6 +57,25 @@ class LlavaImagePixelInputs(TypedDict):
57
in which case the data is passed as a list instead of a batched tensor.
58
"""
59
60
+ feat_is_patch: Union[torch.Tensor, List[torch.Tensor]]
61
+ """
62
+ A boolean mask indicating which image features correspond
63
+ to patch tokens.
64
+
65
+ Shape: `(batch_size, num_crops, num_patch)`
66
+ """
67
+
68
+ embed_is_patch: Union[torch.Tensor, List[torch.Tensor]]
69
+ """
70
+ A boolean mask indicating which image embeddings correspond
71
+ to patch tokens.
72
+
73
+ Shape: `(batch_size, num_embeds)`
74
+ """
75
+
76
+ num_crops: torch.Tensor
77
+ """Shape: `(batch_size, num_images)`"""
78
+
79
80
class LlavaImageEmbeddingInputs(TypedDict):
81
type: Literal["image_embeds"]
@@ -65,6 +85,25 @@ class LlavaImageEmbeddingInputs(TypedDict):
85
`hidden_size` must match the hidden size of language model backbone.
86
"""
87
88
+ feat_is_patch: Union[torch.Tensor, List[torch.Tensor]]
89
+ """
90
+ A boolean mask indicating which image features correspond
91
+ to patch tokens.
92
+
93
+ Shape: `(batch_size, num_crops, num_patch)`
94
+ """
95
+
96
+ embed_is_patch: Union[torch.Tensor, List[torch.Tensor]]
97
+ """
98
+ A boolean mask indicating which image embeddings correspond
99
+ to patch tokens.
100
+
101
+ Shape: `(batch_size, num_embeds)`
102
+ """
103
+
104
+ num_crops: torch.Tensor
105
+ """Shape: `(batch_size, num_images)`"""
106
+
107
108
LlavaImageInputs = Union[LlavaImagePixelInputs, LlavaImageEmbeddingInputs]
109
@@ -317,6 +356,26 @@ class PixtralHFMultiModalProcessor(
356
for p, (h, w) in zip(pixel_values, image_sizes)
357
]
358
359
+ hf_config = self.info.get_hf_config()
360
+
361
+ tile_sizes = [
362
+ get_pixtral_hf_image_feature_grid_size(
363
+ hf_config.vision_config,
364
+ image_width=pixel_value.shape[-1],
365
+ image_height=pixel_value.shape[-2])
366
+ for pixel_value in processed_outputs["pixel_values"]
367
+ ]
368
+ num_crops = torch.tensor([(ncols + 1) * nrows
369
+ for ncols, nrows in tile_sizes])
370
+ # Each image may result to masks of different sizes, so we need to
371
+ # flatten the list and later use `num_crops` to get per-image masks.
372
+ embed_is_patch = torch.tensor(
373
+ flatten_2d_lists([([True] * ncols + [False]) * nrows
374
+ for ncols, nrows in tile_sizes]))
375
+ processed_outputs["num_crops"] = num_crops
376
+ processed_outputs["embed_is_patch"] = embed_is_patch
377
+ processed_outputs["feat_is_patch"] = embed_is_patch
378
+
379
return processed_outputs
380
381
def _get_mm_fields_config(
@@ -324,7 +383,13 @@ class PixtralHFMultiModalProcessor(
383
hf_inputs: BatchFeature,
384
hf_processor_mm_kwargs: Mapping[str, object],
385
) -> Mapping[str, MultiModalFieldConfig]:
386
+ num_crops = hf_inputs.get("num_crops", torch.empty(0)).view(-1)
387
return dict(
388
+ feat_is_patch=MultiModalFieldConfig.flat_from_sizes(
389
+ "image", num_crops),
390
+ embed_is_patch=MultiModalFieldConfig.flat_from_sizes(
391
+ "image", num_crops),
392
+ num_crops=MultiModalFieldConfig.batched("image"),
393
pixel_values=MultiModalFieldConfig.batched("image"),
394
image_embeds=MultiModalFieldConfig.batched("image"),
395
)
@@ -562,6 +627,23 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
627
if pixel_values is None and image_embeds is None:
628
return None
629
630
+ feat_is_patch = kwargs.pop("feat_is_patch", None)
631
+ if feat_is_patch is not None and not isinstance(
632
+ feat_is_patch, (torch.Tensor, list)):
633
+ raise ValueError("Incorrect type of feat_is_patch. "
634
+ f"Got type: {type(feat_is_patch)}")
635
+
636
+ embed_is_patch = kwargs.pop("embed_is_patch", None)
637
+ if embed_is_patch is not None and not isinstance(
638
+ embed_is_patch, (torch.Tensor, list)):
639
+ raise ValueError("Incorrect type of embed_is_patch. "
640
+ f"Got type: {type(embed_is_patch)}")
641
+
642
+ num_crops = kwargs.pop("num_crops", None)
643
+ if num_crops is not None and not isinstance(num_crops, torch.Tensor):
644
+ raise ValueError("Incorrect type of num_crops. "
645
+ f"Got type: {type(num_crops)}")
646
+
647
if pixel_values is not None:
648
if not isinstance(pixel_values, (torch.Tensor, list)):
649
raise ValueError("Incorrect type of pixel values. "
@@ -571,12 +653,18 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
653
return LlavaImagePixelInputs(
654
type="pixel_values",
655
data=flatten_bn(pixel_values),
656
+ feat_is_patch=feat_is_patch,
657
+ embed_is_patch=embed_is_patch,
658
+ num_crops=num_crops,
659
)
660
661
return LlavaImagePixelInputs(
662
type="pixel_values",
663
data=self._validate_pixel_values(
664
flatten_bn(pixel_values, concat=True)),
665
+ feat_is_patch=feat_is_patch,
666
+ embed_is_patch=embed_is_patch,
667
+ num_crops=num_crops,
668
)
669
670
if image_embeds is not None:
@@ -587,6 +675,9 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
675
return LlavaImageEmbeddingInputs(
676
type="image_embeds",
677
data=flatten_bn(image_embeds, concat=True),
678
+ feat_is_patch=feat_is_patch,
679
+ embed_is_patch=embed_is_patch,
680
+ num_crops=num_crops,
681
)
682
683
raise AssertionError("This line should be unreachable.")
@@ -633,16 +724,74 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
724
725
assert self.vision_tower is not None
726
image_features = self._process_image_pixels(image_input)
636
- return self.multi_modal_projector(image_features)
727
638
- def get_multimodal_embeddings(
639
- self, **kwargs
640
- ) -> Union[list[torch.Tensor], torch.Tensor, tuple[torch.Tensor, ...]]:
728
+ if isinstance(image_features, torch.Tensor):
729
+ return self.multi_modal_projector(image_features)
730
+
731
+ feature_sizes = [
732
+ image_feature.shape[0] for image_feature in image_features
733
+ ]
734
+
735
+ image_embeds = self.multi_modal_projector(torch.cat(image_features))
736
+ image_embeds = torch.split(image_embeds, feature_sizes)
737
+ return image_embeds
738
+
739
+ def _get_mm_embeds(
740
+ self,
741
+ features: torch.Tensor, # Shape: (num_crop, num_patch, d)
742
+ feat_is_patch: torch.Tensor, # Shape: (num_crop, num_patch)
743
+ num_crops: torch.Tensor, # Shape: (num_images,)
744
+ embed_is_patch: torch.Tensor, # Shape: (num_embeds,)
745
+ ) -> list[torch.Tensor]:
746
+ """Scatter the patch features into a contiguous tensor that corresponds
747
+ to the embedding tokens defined by the multimodal processor.
748
+
749
+ Mostly copied from `Molmo._get_mm_embeds`. See following fixme comment.
750
+ """
751
+
752
+ # Insert columns of nan values according to `feat_is_patch`. This work
753
+ # ideally should be done in `_process_image_input`, but
754
+ # `_process_image_input` is used in both V0 and V1 path. It's safer to
755
+ # put the logic here.
756
+ # FIXME: Move this logic to `_process_image_input` when v0 is
757
+ # deprecated. Merge this function with `Molmo._get_mm_embeds`.
758
+ feat_is_patch = feat_is_patch.view(-1)
759
+ embed_is_patch = embed_is_patch.view(-1)
760
+ expanded_embedding = torch.full(
761
+ (sum(num_crops), *features.shape[1:]),
762
+ torch.nan,
763
+ dtype=features.dtype).to(features.device)
764
+ expanded_embedding[feat_is_patch] = features
765
+
766
+ num_crops_per_image = num_crops.tolist()
767
+ feats_per_image = expanded_embedding.split(num_crops_per_image)
768
+ f_is_patch_per_image = feat_is_patch.split(num_crops_per_image)
769
+
770
+ embed_dim = expanded_embedding.shape[-1]
771
+ num_embeds = embed_is_patch.shape[0]
772
+
773
+ embeds_in_batch = list[torch.Tensor]()
774
+ for feats, f_is_patch in zip(feats_per_image, f_is_patch_per_image):
775
+ embeds = feats.new_full((num_embeds, embed_dim), torch.nan)
776
+ embeds[embed_is_patch] = feats[f_is_patch]
777
+ embeds_in_batch.append(embeds)
778
+
779
+ return embeds_in_batch
780
+
781
+ def get_multimodal_embeddings(self, **kwargs) -> Optional[NestedTensors]:
782
image_input = self._parse_and_validate_image_input(**kwargs)
783
if image_input is None:
784
return None
785
vision_embeddings = self._process_image_input(image_input)
645
- return vision_embeddings
786
+ if kwargs.get("v0_path", False):
787
+ return vision_embeddings
788
+ else:
789
+ nested_emb = [
790
+ self._get_mm_embeds(*args) for args in zip(
791
+ vision_embeddings, image_input["feat_is_patch"],
792
+ image_input["num_crops"], image_input["embed_is_patch"])
793
+ ]
794
+ return flatten_2d_lists(nested_emb)
795
796
def get_input_embeddings(
797
self,
@@ -651,8 +800,15 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
800
) -> torch.Tensor:
801
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
802
if multimodal_embeddings is not None:
803
+ # Extract the patch tokens
804
+ patch_embeddings = json_map_leaves(
805
+ lambda x: x[~x.isnan()].view(-1, *x.shape[1:]),
806
+ cast(JSONTree[torch.Tensor], multimodal_embeddings),
807
+ )
808
+
809
inputs_embeds = merge_multimodal_embeddings(
655
- input_ids, inputs_embeds, multimodal_embeddings,
810
+ input_ids, inputs_embeds, cast(NestedTensors,
811
+ patch_embeddings),
812
self.config.image_token_index)
813
return inputs_embeds
814
@@ -705,6 +861,7 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
861
# NOTE: In v1, inputs_embeds is always generated at model runner, this
862
# condition is for v0 compatibility.
863
elif inputs_embeds is None:
864
+ kwargs.update({"v0_path": True})
865
vision_embeddings = self.get_multimodal_embeddings(**kwargs)
866
inputs_embeds = self.get_input_embeddings(input_ids,
867
vision_embeddings)
vllm/model_executor/models/molmo.py
+2
-2
@@ -1484,8 +1484,8 @@ class MolmoForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA,
1484
1485
img_patch_id = kwargs.pop("img_patch_id", None)
1486
if not isinstance(img_patch_id, torch.Tensor):
1487
- raise ValueError("Incorrect type of num_crops. "
1488
- f"Got type: {type(num_crops)}")
1487
+ raise ValueError("Incorrect type of img_patch_id. "
1488
+ f"Got type: {type(img_patch_id)}")
1489
self.img_patch_id = img_patch_id.flatten().unique().item()
1490
1491
return MolmoImageInputs(
vllm/model_executor/models/pixtral.py
+8
-2
@@ -1042,9 +1042,13 @@ class PixtralHFVisionModel(nn.Module):
1042
for img in pixel_values
1043
]
1044
1045
+ patch_embeds = [
1046
+ p.flatten(2).permute(0, 2, 1) for p in patch_embeds_list
1047
+ ]
1048
+ embed_sizes = [p.shape[1] for p in patch_embeds]
1049
+
1050
# flatten to a single sequence
1046
- patch_embeds = torch.cat(
1047
- [p.flatten(2).permute(0, 2, 1) for p in patch_embeds_list], dim=1)
1051
+ patch_embeds = torch.cat(patch_embeds, dim=1)
1052
patch_embeds = self.ln_pre(patch_embeds)
1053
1054
# positional embeddings
@@ -1075,6 +1079,8 @@ class PixtralHFVisionModel(nn.Module):
1079
out = resolve_visual_encoder_outputs(out, feature_sample_layers, None,
1080
self.config.num_hidden_layers)
1081
1082
+ # squeeze dim 0 and split into separate tensors for each image
1083
+ out = torch.split(torch.squeeze(out), embed_sizes)
1084
return out
1085
1086
# (TODO) Add prefix argument for filtering out weights to be loaded