main
py 621 lines 21.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import math
5 from collections.abc import Iterable, Mapping, Sequence
6 from typing import Annotated, Any, Literal, cast
7
8 import torch
9 import torch.nn as nn
10 from transformers import BatchFeature, Mistral3Config, PixtralVisionConfig
11 from transformers.models.pixtral import PixtralProcessor
12
13 from vllm.config import VllmConfig
14 from vllm.config.multimodal import MultiModalDummyOptions
15 from vllm.inputs import MultiModalDataDict
16 from vllm.model_executor.layers.activation import get_act_fn
17 from vllm.model_executor.layers.fusion.mm_input_norm import build_mm_input_norm
18 from vllm.model_executor.layers.layernorm import RMSNorm
19 from vllm.model_executor.layers.linear import ColumnParallelLinear, RowParallelLinear
20 from vllm.model_executor.layers.quantization import QuantizationConfig
21 from vllm.model_executor.models.module_mapping import MultiModelKeys
22 from vllm.multimodal import MULTIMODAL_REGISTRY
23 from vllm.multimodal.inputs import (
24 MultiModalFieldConfig,
25 MultiModalKwargsItems,
26 )
27 from vllm.multimodal.parse import (
28 ImageProcessorItems,
29 ImageSize,
30 MultiModalDataItems,
31 )
32 from vllm.multimodal.processing import (
33 BaseDummyInputsBuilder,
34 BaseMultiModalProcessor,
35 BaseProcessingInfo,
36 PromptReplacement,
37 PromptUpdate,
38 PromptUpdateDetails,
39 )
40 from vllm.sequence import IntermediateTensors
41 from vllm.utils.tensor_schema import TensorSchema, TensorShape
42
43 from .interfaces import (
44 MultiModalEmbeddings,
45 SupportsEagle,
46 SupportsEagle3,
47 SupportsLoRA,
48 SupportsMultiModal,
49 SupportsPP,
50 )
51 from .pixtral import PixtralHFEncoderInfo, PixtralHFVisionModel
52 from .utils import (
53 AutoWeightsLoader,
54 WeightsMapper,
55 get_layer_index,
56 init_vllm_registered_model,
57 maybe_prefix,
58 )
59
60
61 class Mistral3ImagePixelInputs(TensorSchema):
62 """Dimensions:
63 - bn: Batch size * number of images
64 - c: Number of channels (3)
65 - h: Height of each image
66 - w: Width of each image
67 """
68
69 type: Literal["pixel_values_pixtral"] = "pixel_values_pixtral"
70
71 # Note that `height` or `width` may be different per batch and image,
72 # in which case the data is passed as a list instead of a batched tensor.
73 pixel_values: Annotated[
74 torch.Tensor | list[torch.Tensor],
75 TensorShape("bn", 3, "h", "w", dynamic_dims={"h", "w"}),
76 ]
77
78
79 class Mistral3PatchMerger(nn.Module):
80 """Learned merging of spatial_merge_size ** 2 patches."""
81
82 def __init__(
83 self, vision_hidden_size: int, spatial_merge_size: int, patch_size: int
84 ):
85 super().__init__()
86
87 self.vision_hidden_size = vision_hidden_size
88 self.spatial_merge_size = spatial_merge_size
89 self.patch_size = patch_size
90 self.merging_layer = nn.Linear(
91 vision_hidden_size * self.spatial_merge_size**2,
92 vision_hidden_size,
93 bias=False,
94 )
95
96 def forward(
97 self, image_features: torch.Tensor, image_sizes: torch.Tensor
98 ) -> torch.Tensor:
99 image_sizes = [
100 (image_size[0] // self.patch_size, image_size[1] // self.patch_size)
101 for image_size in image_sizes
102 ]
103
104 tokens_per_image = [h * w for h, w in image_sizes]
105 d = image_features.shape[-1]
106
107 permuted_tensor = []
108 for image_index, image_tokens in enumerate(
109 image_features.split(tokens_per_image)
110 ):
111 # Reshape image_tokens into a 2D grid
112 h, w = image_sizes[image_index]
113 image_grid = image_tokens.view(h, w, d).permute(2, 0, 1).unsqueeze(0)
114 grid = torch.nn.functional.unfold(
115 image_grid,
116 kernel_size=self.spatial_merge_size,
117 stride=self.spatial_merge_size,
118 )
119 grid = grid.view(d * self.spatial_merge_size**2, -1).t()
120 permuted_tensor.append(grid)
121
122 image_features = torch.cat(permuted_tensor, dim=0)
123 image_features = self.merging_layer(image_features)
124 return image_features
125
126
127 class Mistral3MultiModalProjector(nn.Module):
128 def __init__(
129 self,
130 vision_hidden_size: int,
131 text_hidden_size: int,
132 spatial_merge_size: int,
133 patch_size: int,
134 projector_hidden_act: str,
135 multimodal_projector_bias: bool,
136 quant_config: QuantizationConfig | None = None,
137 prefix: str = "",
138 ):
139 super().__init__()
140
141 self.norm = RMSNorm(vision_hidden_size, eps=1e-5)
142 self.patch_merger = Mistral3PatchMerger(
143 vision_hidden_size=vision_hidden_size,
144 spatial_merge_size=spatial_merge_size,
145 patch_size=patch_size,
146 )
147
148 self.linear_1 = ColumnParallelLinear(
149 vision_hidden_size,
150 text_hidden_size,
151 bias=multimodal_projector_bias,
152 quant_config=quant_config,
153 prefix=f"{prefix}.linear_1",
154 )
155 self.act = get_act_fn(projector_hidden_act)
156 self.linear_2 = RowParallelLinear(
157 text_hidden_size,
158 text_hidden_size,
159 bias=multimodal_projector_bias,
160 quant_config=quant_config,
161 prefix=f"{prefix}.linear_2",
162 )
163
164 def forward(
165 self, image_features: torch.Tensor, image_sizes: torch.Tensor
166 ) -> torch.Tensor:
167 image_features = self.norm(image_features)
168 image_features = self.patch_merger(image_features, image_sizes)
169 hidden_states, _ = self.linear_1(image_features)
170 hidden_states = self.act(hidden_states)
171 hidden_states, _ = self.linear_2(hidden_states)
172 return hidden_states
173
174
175 class Mistral3HFEncoderInfo(PixtralHFEncoderInfo):
176 def __init__(self, hf_config: Mistral3Config, image_size: int) -> None:
177 super().__init__(hf_config)
178 self.image_size = image_size
179
180 def get_image_size(self) -> int:
181 return self.image_size
182
183 def get_patch_grid_size(
184 self,
185 *,
186 image_width: int,
187 image_height: int,
188 ) -> tuple[int, int]:
189 max_size = self.get_image_size()
190 ratio = max(image_width / max_size, image_height / max_size)
191
192 if ratio > 1:
193 image_width = math.floor(image_width / ratio)
194 image_height = math.floor(image_height / ratio)
195
196 assert isinstance(self.hf_config, Mistral3Config)
197 merged_patch_size = (
198 self.vision_config.patch_size * self.hf_config.spatial_merge_size
199 )
200
201 ncols = (image_width - 1) // merged_patch_size + 1
202 nrows = (image_height - 1) // merged_patch_size + 1
203 return ncols, nrows
204
205
206 class Mistral3ProcessingInfo(BaseProcessingInfo):
207 def get_hf_config(self) -> Mistral3Config:
208 return self.ctx.get_hf_config(Mistral3Config)
209
210 def get_vision_encoder_info(
211 self,
212 mm_processor_kwargs: Mapping[str, object] | None = None,
213 ) -> PixtralHFEncoderInfo:
214 processor = self.get_hf_processor()
215 size = processor.image_processor.size
216 merged_mm_kwargs = self._merge_and_resolve_mm_processor_kwargs(
217 mm_processor_kwargs or {}
218 )
219 image_mm_kwargs = cast(
220 Mapping[str, Any],
221 merged_mm_kwargs.get("images_kwargs", {}),
222 )
223 if override_size := image_mm_kwargs.get("size"):
224 size = size | override_size
225
226 image_size = size["longest_edge"]
227 return Mistral3HFEncoderInfo(self.get_hf_config(), image_size)
228
229 def get_hf_processor(self, **kwargs: object) -> PixtralProcessor:
230 return self.ctx.get_hf_processor(PixtralProcessor, **kwargs)
231
232 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
233 return {"image": None}
234
235 def get_num_image_tokens(
236 self,
237 *,
238 image_width: int,
239 image_height: int,
240 ) -> int:
241 vision_encoder_info = self.get_vision_encoder_info()
242 return vision_encoder_info.get_num_image_tokens(
243 image_width=image_width,
244 image_height=image_height,
245 )
246
247 def get_image_size_with_most_features(self) -> ImageSize:
248 vision_encoder_info = self.get_vision_encoder_info()
249 width = height = vision_encoder_info.get_image_size()
250 return ImageSize(width=width, height=height)
251
252
253 class Mistral3DummyInputsBuilder(BaseDummyInputsBuilder):
254 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
255 num_images = mm_counts.get("image", 0)
256
257 processor = self.info.get_hf_processor()
258 image_token = processor.image_token
259
260 return image_token * num_images
261
262 def get_dummy_mm_data(
263 self,
264 seq_len: int,
265 mm_counts: Mapping[str, int],
266 mm_options: MultiModalDummyOptions,
267 ) -> MultiModalDataDict:
268 target_width, target_height = self.info.get_image_size_with_most_features()
269
270 return {
271 "image": self._get_dummy_images(
272 width=target_width,
273 height=target_height,
274 num_images=mm_counts.get("image", 0),
275 overrides=mm_options.get("image"),
276 )
277 }
278
279
280 class Mistral3MultiModalProcessor(BaseMultiModalProcessor[Mistral3ProcessingInfo]):
281 def _postprocess_hf_mm_data(
282 self,
283 mm_data: Mapping[str, object],
284 hf_processor_mm_kwargs: Mapping[str, object],
285 processed_data: BatchFeature,
286 ) -> BatchFeature:
287 if not mm_data:
288 return processed_data
289
290 pixel_values = processed_data.get("pixel_values")
291 if pixel_values is not None:
292 # Avoid padding since we need the output for each image to be
293 # independent of other images for the cache to work correctly
294 image_sizes = processed_data["image_sizes"]
295 assert len(pixel_values) == len(image_sizes)
296
297 processed_data["pixel_values"] = [
298 p[:, :h, :w] for p, (h, w) in zip(pixel_values, image_sizes)
299 ]
300
301 return processed_data
302
303 def _get_mm_fields_config(
304 self,
305 hf_inputs: BatchFeature,
306 hf_processor_mm_kwargs: Mapping[str, object],
307 ) -> Mapping[str, MultiModalFieldConfig]:
308 return dict(
309 pixel_values=MultiModalFieldConfig.batched("image"),
310 image_embeds=MultiModalFieldConfig.batched("image"),
311 )
312
313 def _get_prompt_updates(
314 self,
315 mm_items: MultiModalDataItems,
316 hf_processor_mm_kwargs: Mapping[str, object],
317 out_mm_kwargs: MultiModalKwargsItems,
318 ) -> Sequence[PromptUpdate]:
319 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
320 hf_config = self.info.get_hf_config()
321 tokenizer = self.info.get_tokenizer()
322 vocab = tokenizer.get_vocab()
323
324 image_break_id = vocab[processor.image_break_token]
325 image_token_id = hf_config.image_token_index
326 image_end_id = vocab[processor.image_end_token]
327
328 assert isinstance(hf_config.vision_config, PixtralVisionConfig)
329 encoder_info = self.info.get_vision_encoder_info(hf_processor_mm_kwargs)
330
331 def get_replacement(item_idx: int):
332 images = mm_items.get_items("image", ImageProcessorItems)
333 image_size = images.get_image_size(item_idx)
334
335 ncols, nrows = encoder_info.get_patch_grid_size(
336 image_width=image_size.width,
337 image_height=image_size.height,
338 )
339
340 tokens = ([image_token_id] * ncols + [image_break_id]) * nrows
341 tokens[-1] = image_end_id
342
343 return PromptUpdateDetails.select_token_id(tokens, image_token_id)
344
345 return [
346 PromptReplacement(
347 modality="image",
348 target=[image_token_id],
349 replacement=get_replacement,
350 ),
351 ]
352
353
354 def _get_num_hidden_layers(hf_config: Mistral3Config) -> int:
355 """Determine the number of hidden layers to initialize up to in the
356 visual encoder.
357
358 Args:
359 hf_config: Model config with vision feature layer(s).
360
361 """
362 feature_layers = hf_config.vision_feature_layer
363 num_hidden_layers = hf_config.vision_config.num_hidden_layers
364 # If we have one feature layer, initialize up to that layer
365 if isinstance(feature_layers, int):
366 return get_layer_index(feature_layers, num_hidden_layers)
367 # If we have multiple feature layers, initialize up to the deepest one
368 elif isinstance(feature_layers, (list, tuple)):
369 return max(get_layer_index(idx, num_hidden_layers) for idx in feature_layers)
370 raise TypeError(
371 f"vision_layer_feature type: {type(feature_layers)} is not supported"
372 )
373
374
375 def init_vision_tower_for_mistral3(
376 hf_config: Mistral3Config,
377 quant_config: QuantizationConfig | None,
378 *,
379 input_norm: nn.Module | None = None,
380 require_post_norm: bool | None = None,
381 prefix: str = "",
382 ) -> PixtralHFVisionModel:
383 vision_config = hf_config.vision_config
384
385 # Initialize the vision tower only up to the deepest required feature layer
386 num_hidden_layers = _get_num_hidden_layers(hf_config)
387
388 assert isinstance(vision_config, PixtralVisionConfig)
389
390 return PixtralHFVisionModel(
391 vision_config,
392 quant_config=quant_config,
393 input_norm=input_norm,
394 num_hidden_layers_override=num_hidden_layers,
395 require_post_norm=require_post_norm,
396 prefix=prefix,
397 )
398
399
400 @MULTIMODAL_REGISTRY.register_processor(
401 Mistral3MultiModalProcessor,
402 info=Mistral3ProcessingInfo,
403 dummy_inputs=Mistral3DummyInputsBuilder,
404 )
405 class Mistral3ForConditionalGeneration(
406 nn.Module,
407 SupportsLoRA,
408 SupportsMultiModal,
409 SupportsPP,
410 SupportsEagle,
411 SupportsEagle3,
412 ):
413 supports_mm_device_do_normalize = True
414
415 packed_modules_mapping = {
416 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
417 "gate_up_proj": ["gate_proj", "up_proj"],
418 }
419
420 hf_to_vllm_mapper = WeightsMapper(
421 orig_to_new_prefix={
422 # mapping for new names in checkpoint saved after transformers v4.52
423 "model.language_model.": "language_model.model.",
424 "model.vision_tower.": "vision_tower.",
425 "model.multi_modal_projector.": "multi_modal_projector.",
426 "lm_head.": "language_model.lm_head.",
427 # Some PEFT LoRAs are trained against the text submodule directly
428 # and produce names like `base_model.model.model.layers.*`.
429 "model.": "language_model.model.",
430 },
431 orig_to_new_suffix={
432 # FP8 quantized HF checkpoints use "activation_scale" and
433 # "weight_scale_inv" but vLLM's FP8 linear layers register
434 # them as "input_scale" and "weight_scale"
435 ".activation_scale": ".input_scale",
436 ".weight_scale_inv": ".weight_scale",
437 },
438 )
439
440 @classmethod
441 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
442 if modality.startswith("image"):
443 return None
444
445 raise ValueError("Only image modality is supported")
446
447 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
448 super().__init__()
449
450 config = vllm_config.model_config.hf_config
451 quant_config = vllm_config.quant_config
452 multimodal_config = vllm_config.model_config.multimodal_config
453
454 self.config = config
455 self.multimodal_config = multimodal_config
456
457 # NOTE: This is a special case for Pixtral-12B in the HF-format
458 # https://huggingface.co/mistral-community/pixtral-12b/blob/main/config.json # noqa
459 if (
460 config.projector_hidden_act is None
461 and config.vision_config.hidden_act == "gelu"
462 ):
463 config.projector_hidden_act = "gelu"
464
465 with self._mark_tower_model(vllm_config, "image"):
466 self.vision_tower = init_vision_tower_for_mistral3(
467 config,
468 quant_config=quant_config,
469 input_norm=build_mm_input_norm(vllm_config.model_config),
470 require_post_norm=False,
471 prefix=maybe_prefix(prefix, "vision_tower"),
472 )
473 self.multi_modal_projector = Mistral3MultiModalProjector(
474 vision_hidden_size=config.vision_config.hidden_size,
475 text_hidden_size=config.text_config.hidden_size,
476 projector_hidden_act=config.projector_hidden_act,
477 spatial_merge_size=config.spatial_merge_size,
478 patch_size=config.vision_config.patch_size,
479 multimodal_projector_bias=config.multimodal_projector_bias,
480 quant_config=quant_config,
481 prefix=maybe_prefix(prefix, "multi_modal_projector"),
482 )
483
484 with self._mark_language_model(vllm_config):
485 self.language_model = init_vllm_registered_model(
486 vllm_config=vllm_config,
487 hf_config=config.text_config,
488 prefix=maybe_prefix(prefix, "language_model"),
489 )
490
491 self.make_empty_intermediate_tensors = (
492 self.language_model.make_empty_intermediate_tensors
493 )
494
495 def _parse_and_validate_image_input(
496 self, **kwargs: object
497 ) -> Mistral3ImagePixelInputs | None:
498 pixel_values = kwargs.pop("pixel_values", None)
499 image_embeds = kwargs.pop("image_embeds", None)
500
501 if pixel_values is None and image_embeds is None:
502 return None
503
504 return Mistral3ImagePixelInputs(
505 type="pixel_values_pixtral",
506 pixel_values=pixel_values,
507 )
508
509 def _process_image_input(
510 self,
511 image_input: Mistral3ImagePixelInputs,
512 ) -> torch.Tensor | tuple[torch.Tensor, ...]:
513 if image_input["type"] == "image_embeds":
514 return image_input["data"]
515
516 image_sizes = [
517 (img.shape[-2], img.shape[-1]) for img in image_input["pixel_values"]
518 ]
519
520 image_features = self.vision_tower(image_input["pixel_values"])
521
522 if isinstance(image_features, torch.Tensor):
523 return self.multi_modal_projector(image_features, image_sizes)
524
525 feature_sizes = [
526 image_feature.shape[0] // self.config.spatial_merge_size**2
527 for image_feature in image_features
528 ]
529
530 image_embeds = self.multi_modal_projector(
531 torch.cat(image_features), image_sizes
532 )
533 if len(feature_sizes) > 1:
534 image_embeds = torch.split(image_embeds, feature_sizes)
535 else:
536 image_embeds = (image_embeds,)
537 return image_embeds
538
539 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
540 image_input = self._parse_and_validate_image_input(**kwargs)
541 if image_input is None:
542 return []
543
544 vision_embeddings = self._process_image_input(image_input)
545
546 return vision_embeddings
547
548 def forward(
549 self,
550 input_ids: torch.Tensor | None,
551 positions: torch.Tensor,
552 intermediate_tensors: IntermediateTensors | None = None,
553 inputs_embeds: torch.Tensor | None = None,
554 **kwargs: object,
555 ) -> torch.Tensor | IntermediateTensors:
556 """Run forward pass for Mistral3.
557
558 One key thing to understand is the `input_ids` already accounts for the
559 positions of the to-be-inserted image embeddings.
560
561 Concretely, consider a text prompt:
562 `"USER: <image>\\nWhat's the content of the image?\\nASSISTANT:"`.
563
564 Tokenizer outputs:
565 `[1, 3148, 1001, 29901, 29871, 32000, 29871, 13, 5618, 29915, 29879,
566 278, 2793, 310, 278, 1967, 29973, 13, 22933, 9047, 13566, 29901]`.
567
568 To reserve space in KV cache, we have to insert placeholder tokens
569 before they are inputted to the model, so the input processor prepends
570 additional image tokens (denoted as `32000`), resulting in:
571 `[1, 3148, 1001, 29901, 29871, 32000, ..., 32000, 29871, 13, 5618,
572 29915, 29879, 278, 2793, 310, 278, 1967, 29973, 13, 22933, 9047, 13566,
573 29901]`.
574
575 We insert 575 tokens so that including the original image token in the
576 input, there are a total of 576 (24 * 24) image tokens, which
577 corresponds to the number of image tokens inputted to the language
578 model, i.e. the number of image tokens outputted by the visual encoder.
579
580 This way, the `positions` and `attn_metadata` are consistent
581 with the `input_ids`.
582
583 Args:
584 input_ids: Flattened (concatenated) input_ids corresponding to a
585 batch.
586 positions: Position indices for the input tokens.
587 intermediate_tensors: Intermediate tensors from prior forward pass.
588 inputs_embeds: Optional tensor of input embeddings.
589 **kwargs: Multimodal inputs for this batch, forwarded to the
590 multimodal embedding path.
591
592 Info:
593 [`Mistral3ImagePixelInputs`][vllm.model_executor.models.mistral3.Mistral3ImagePixelInputs]
594
595 """
596 if intermediate_tensors is not None:
597 inputs_embeds = None
598
599 hidden_states = self.language_model.model(
600 input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds
601 )
602
603 return hidden_states
604
605 def compute_logits(
606 self,
607 hidden_states: torch.Tensor,
608 ) -> torch.Tensor | None:
609 return self.language_model.compute_logits(hidden_states)
610
611 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
612 loader = AutoWeightsLoader(self)
613 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
614
615 def get_mm_mapping(self) -> MultiModelKeys:
616 """Get the module prefix in multimodal models."""
617 return MultiModelKeys.from_string_field(
618 language_model="language_model",
619 connector="multi_modal_projector",
620 tower_model="vision_tower",
621 )