main
py 973 lines 41.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Copyright 2024 The vLLM team.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 """Transformers modeling backend mixin for multi-modal models."""
18
19 from collections import defaultdict
20 from collections.abc import Iterable, Mapping, Sequence
21 from contextlib import ExitStack, contextmanager
22 from typing import TYPE_CHECKING, Any
23
24 import torch
25 import transformers
26 from transformers.utils.generic import ModelOutput
27
28 from vllm.compilation.decorators import should_torch_compile_mm_encoder
29 from vllm.config.utils import getattr_iter
30 from vllm.inputs import MultiModalDataBuiltins, MultiModalDataDict
31 from vllm.logger import init_logger
32 from vllm.model_executor.models.interfaces import (
33 MultiModalEmbeddings,
34 SupportsMRoPE,
35 SupportsMultiModal,
36 )
37 from vllm.model_executor.models.module_mapping import MultiModelKeys
38 from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalKwargsItems
39 from vllm.multimodal.inputs import MultiModalFeatureSpec, MultiModalFieldConfig
40 from vllm.multimodal.parse import ImageSize, MultiModalDataItems, MultiModalDataParser
41 from vllm.multimodal.processing import (
42 BaseDummyInputsBuilder,
43 BaseMultiModalProcessor,
44 BaseProcessingInfo,
45 PromptReplacement,
46 PromptUpdate,
47 PromptUpdateDetails,
48 cached_encode,
49 )
50 from vllm.sequence import IntermediateTensors
51 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
52 from vllm.utils.torch_utils import async_tensor_h2d
53
54 from .base import Base
55
56 if TYPE_CHECKING:
57 from transformers import BatchFeature, PreTrainedModel
58
59 from vllm.config import VllmConfig
60 from vllm.config.multimodal import MultiModalDummyOptions
61
62 logger = init_logger(__name__)
63
64 _MODALITY_TO_TOKEN_TYPE_ID = {"image": 1, "video": 2, "audio": 3}
65 _MODALITY_SIZE_KEYS = {"audio": "num_audio_tokens", "image": "num_image_patches"}
66
67
68 def _get_embed_token_id(replacement_ids: torch.Tensor) -> int:
69 """The token an expansion repeats is the one holding the embeddings."""
70 return int(replacement_ids.mode().values)
71
72
73 class MultiModalProcessingInfo(BaseProcessingInfo):
74 def _get_audio_processor(self) -> Any:
75 # TODO: drop feature_extractor branch once huggingface/transformers#44394 lands.
76 return getattr_iter(
77 self.get_hf_processor(), ("audio_processor", "feature_extractor")
78 )
79
80 def _is_audio_model(self) -> bool:
81 return self._get_audio_processor() is not None
82
83 def _is_image_model(self) -> bool:
84 return hasattr(self.get_hf_processor(), "image_processor")
85
86 def _get_supported_modalities(self) -> list[str]:
87 modalities = []
88 if self._is_audio_model():
89 modalities.append("audio")
90 if self._is_image_model():
91 modalities.append("image")
92 if not modalities:
93 raise ValueError(
94 f"{type(self.get_hf_processor()).__name__} exposes neither an image "
95 "processor nor an audio processor, so the Transformers modeling "
96 "backend cannot serve this model as multi-modal."
97 )
98 return modalities
99
100 def _get_audio_sampling_rate(self) -> float:
101 sub = self._get_audio_processor()
102 if sub is not None and hasattr(sub, "sampling_rate"):
103 return sub.sampling_rate
104 return 16000.0
105
106 def get_data_parser(self) -> MultiModalDataParser:
107 if self._is_audio_model():
108 return MultiModalDataParser(
109 target_sr=self._get_audio_sampling_rate(),
110 expected_hidden_size=self._get_expected_hidden_size(),
111 )
112 return super().get_data_parser()
113
114 def get_supported_mm_limits(self):
115 return dict.fromkeys(self._get_supported_modalities())
116
117 def get_mm_max_tokens_per_item(self, seq_len, mm_counts):
118 modalities = self._get_supported_modalities()
119 max_tokens = {}
120 if "audio" in modalities:
121 max_tokens["audio"] = self.get_max_audio_tokens()
122 if "image" in modalities:
123 max_tokens["image"] = self.get_max_image_tokens()
124 return max_tokens
125
126 def get_max_audio_tokens(self) -> int:
127 config = self.get_hf_config()
128 audio_config_names = ("audio_config", "encoder_config")
129 names = ("max_source_positions", "max_position_embeddings", "max_pos_emb")
130 audio_config = getattr_iter(config, audio_config_names, default=config)
131 val = getattr_iter(audio_config, names)
132 if val is not None:
133 return int(val)
134 raise ValueError(
135 f"Unable to get max input length from {type(audio_config).__name__}. "
136 f"The following attribute names were checked: {names}."
137 )
138
139 def get_max_image_tokens(self) -> int:
140 width, height = self.get_image_size_with_most_features()
141 processor = self.get_hf_processor()
142 multimodal_config = self.ctx.model_config.get_multimodal_config()
143 mm_processor_kwargs = multimodal_config.mm_processor_kwargs or {}
144 mm_tokens = processor._get_num_multimodal_tokens(
145 image_sizes=([height, width],), **mm_processor_kwargs
146 )
147 image_tokens = mm_tokens["num_image_tokens"][0]
148 return image_tokens
149
150 def get_image_size_with_most_features(self) -> ImageSize:
151 return ImageSize(width=10_000, height=10_000) # arbitrary very large size
152
153
154 class MultiModalDummyInputsBuilder(BaseDummyInputsBuilder[MultiModalProcessingInfo]):
155 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
156 text = ""
157 if self.info._is_audio_model() and (num_audios := mm_counts.get("audio", 0)):
158 processor = self.info.get_hf_processor()
159 audio_token = getattr(processor, "audio_token", "")
160 # Separated so that adjacent placeholders stay distinguishable
161 text += " ".join([audio_token] * num_audios)
162 if self.info._is_image_model() and (num_images := mm_counts.get("image", 0)):
163 processor = self.info.get_hf_processor()
164 if "gemma3" in processor.__class__.__name__.lower():
165 image_token = processor.boi_token
166 else:
167 image_token = getattr(processor, "image_token", "")
168 # Some processors (e.g. HunYuanVL) reject a bare image token and
169 # require each one to be wrapped in its start/end markers.
170 start_token = getattr(processor, "image_start_token", "")
171 end_token = getattr(processor, "image_end_token", "")
172 image_token = f"{start_token}{image_token}{end_token}"
173 text += image_token * num_images
174 return text
175
176 def get_dummy_mm_data(
177 self,
178 seq_len: int,
179 mm_counts: Mapping[str, int],
180 mm_options: "MultiModalDummyOptions",
181 ) -> MultiModalDataDict:
182 data = MultiModalDataBuiltins()
183 if self.info._is_audio_model() and (num_audios := mm_counts.get("audio", 0)):
184 sampling_rate = self.info._get_audio_sampling_rate()
185 sub = self.info._get_audio_processor()
186 chunk_length = getattr(sub, "chunk_length", None) if sub else None
187 if chunk_length is None:
188 chunk_length = 30
189 data["audio"] = self._get_dummy_audios(
190 length=int(chunk_length * sampling_rate),
191 num_audios=num_audios,
192 overrides=mm_options.get("audio"),
193 )
194 if self.info._is_image_model() and (num_images := mm_counts.get("image", 0)):
195 width, height = self.info.get_image_size_with_most_features()
196 data["image"] = self._get_dummy_images(
197 width=width,
198 height=height,
199 num_images=num_images,
200 overrides=mm_options.get("image"),
201 )
202 return data
203
204
205 class MultiModalProcessor(BaseMultiModalProcessor[MultiModalProcessingInfo]):
206 """Locates placeholders from the `text_replacement_offsets` the HF processor
207 reports, expressing each one as a `PromptUpdate`.
208
209 Stating the expansion as an update is what lets it be rebuilt from an
210 unexpanded prompt, so this processor takes the base class's processing path
211 and with it the multi-modal processor cache.
212 """
213
214 def _get_modality_field_names(self, modality: str) -> set[str]:
215 """Names of the fields the sub-processor for `modality` produces."""
216 # TODO: use else branch only once huggingface/transformers#44394 lands.
217 if modality == "audio":
218 sub_processor = self.info._get_audio_processor()
219 else:
220 processor = self.info.get_hf_processor()
221 sub_processor = getattr(processor, f"{modality}_processor", None)
222
223 # Pre-computed embeddings bypass the sub-processor entirely
224 names = {f"{modality}_embeds"}
225 for name in getattr(sub_processor, "model_input_names", None) or ():
226 # Companion masks are emitted but not always declared
227 names.update((name, f"{name}_mask"))
228 return names
229
230 def _partition_keys_by_modality(
231 self,
232 keys: list[str],
233 modalities: list[str],
234 ) -> dict[str, list[str]]:
235 """Attribute each HF processor output key to the modality that produced it."""
236 if len(modalities) == 1:
237 return {modalities[0]: keys}
238
239 claimed = {m: self._get_modality_field_names(m) for m in modalities}
240
241 owned: dict[str, list[str]] = {modality: [] for modality in modalities}
242 unclaimed = []
243 for key in keys:
244 for modality in modalities:
245 if key in claimed[modality]:
246 owned[modality].append(key)
247 break
248 else:
249 unclaimed.append(key)
250
251 if unclaimed:
252 logger.warning_once(
253 "Unable to attribute %s to any of the modalities %s, so they "
254 "will not be passed to the model. Add them to the relevant "
255 "sub-processor's `model_input_names` to fix this.",
256 tuple(unclaimed),
257 tuple(modalities),
258 )
259
260 return owned
261
262 def _get_slice_dim(self, data: Any, total_rows: int) -> int:
263 """Which dimension of a field holds the rows belonging to each item.
264
265 Some processors (e.g., Idefics3) return image fields with a leading batch
266 dimension, putting the rows one dimension further in.
267 """
268 if not isinstance(data, torch.Tensor) or data.ndim < 2:
269 return 0
270 if data.shape[0] != total_rows and data.shape[1] == total_rows:
271 return 1
272 return 0
273
274 def _get_mm_fields_config(
275 self,
276 hf_inputs: "BatchFeature",
277 hf_processor_mm_kwargs: Mapping[str, object],
278 ) -> Mapping[str, MultiModalFieldConfig]:
279 # HF Processors always return a mask but vLLM doesn't need it
280 hf_inputs.pop("attention_mask", None)
281
282 # Absent if the modality had no items
283 sizes = {
284 modality: hf_inputs.get(key)
285 for modality, key in _MODALITY_SIZE_KEYS.items()
286 }
287 modalities = [m for m, size in sizes.items() if size is not None]
288
289 # Keys we wrote ourselves, rather than ones a sub-processor produced
290 own_keys = set(_MODALITY_SIZE_KEYS.values()) | {
291 f"{modality}_replacement_{suffix}"
292 for modality in modalities
293 for suffix in ("ids", "sizes")
294 }
295 keys = [key for key in hf_inputs if key not in own_keys]
296 owned = self._partition_keys_by_modality(keys, modalities)
297
298 # Un-padded fields are already one entry per item, so index rather than slice
299 mm_fields: dict[str, MultiModalFieldConfig] = {
300 key: MultiModalFieldConfig.batched(modality)
301 if modality == "audio" or isinstance(hf_inputs[key], list)
302 else MultiModalFieldConfig.flat_from_sizes(
303 modality,
304 sizes[modality],
305 dim=self._get_slice_dim(hf_inputs[key], int(sizes[modality].sum())),
306 )
307 for modality in modalities
308 for key in owned[modality]
309 }
310
311 for modality in modalities:
312 # One row per item, and only ever read on the CPU
313 mm_fields[_MODALITY_SIZE_KEYS[modality]] = MultiModalFieldConfig.batched(
314 modality, keep_on_cpu=True
315 )
316 replacement_sizes = hf_inputs.get(f"{modality}_replacement_sizes")
317 if replacement_sizes is not None:
318 mm_fields[f"{modality}_replacement_ids"] = (
319 MultiModalFieldConfig.flat_from_sizes(
320 modality, replacement_sizes, keep_on_cpu=True
321 )
322 )
323
324 if "image" in modalities:
325 # Always one row per item, whatever they describe
326 mm_fields["image_grid_thw"] = MultiModalFieldConfig.batched(
327 "image", keep_on_cpu=True
328 )
329 # TODO: route to "video" once the video modality is supported
330 mm_fields["video_grid_thw"] = MultiModalFieldConfig.batched(
331 "image", keep_on_cpu=True
332 )
333
334 return mm_fields
335
336 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
337 return self.dummy_inputs.get_dummy_text(mm_counts)
338
339 def _unpad_audios(
340 self,
341 hf_inputs: "BatchFeature",
342 mm_data: Mapping[str, object],
343 mm_kwargs: Mapping[str, object],
344 ) -> None:
345 """Replace the audio fields with each audio processed on its own.
346
347 Processors pad every audio up to the longest in the call, which would leave
348 an item's data dependent on what it was processed with. Unlike images,
349 nothing in the output states how long each one really is, and processors
350 pad a lone audio too, so the only way to know what an audio produces by
351 itself is to process it by itself.
352 """
353 audios = mm_data.get("audio")
354 if not isinstance(audios, Iterable) or not audios:
355 return
356 if len({len(audio) for audio in audios}) == 1:
357 return
358
359 alone = [
360 self.info.ctx.call_hf_processor(
361 self.info.get_hf_processor(**mm_kwargs),
362 dict(
363 text=self.dummy_inputs.get_dummy_text({"audio": 1}), audio=[audio]
364 ),
365 mm_kwargs,
366 )
367 for audio in audios
368 ]
369
370 for key in self._get_modality_field_names("audio"):
371 if isinstance(hf_inputs.get(key), torch.Tensor):
372 hf_inputs[key] = [output[key][0] for output in alone]
373
374 def _unpad_images(self, hf_inputs: "BatchFeature") -> None:
375 """Trim each image back to its own size when the processor padded them all
376 to the largest in the batch.
377
378 An image's data has to depend on nothing but that image, or the multi-modal
379 processor cache would store it under that image's hash and later reuse it
380 beside a different neighbour. Padding is re-applied when the encoder runs.
381 """
382 pixel_values = hf_inputs.get("pixel_values")
383 image_sizes = hf_inputs.get("image_sizes")
384 if not isinstance(pixel_values, torch.Tensor):
385 return
386 if not isinstance(image_sizes, torch.Tensor):
387 return
388 if pixel_values.ndim != 4 or len(pixel_values) != len(image_sizes):
389 return
390
391 # The sizes describe the trailing dimensions only if the largest of them is
392 # what the batch was padded up to. Otherwise they mean something else, as
393 # in llava-onevision, where they are the sizes before any processing.
394 maxima = image_sizes.max(dim=0).values
395 if maxima.tolist() != list(pixel_values.shape[-2:]):
396 return
397
398 hf_inputs["pixel_values"] = [
399 image[..., :height, :width]
400 for image, (height, width) in zip(pixel_values, image_sizes.tolist())
401 ]
402
403 def _get_prompt_updates(
404 self,
405 mm_items: MultiModalDataItems,
406 hf_processor_mm_kwargs: Mapping[str, object],
407 out_mm_kwargs: MultiModalKwargsItems,
408 ) -> Sequence[PromptUpdate]:
409 """Replace each modality's placeholder token with the token ids that item's
410 replacement text encodes to, marking which of them hold embeddings."""
411 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
412 tokenizer = self.info.get_tokenizer()
413
414 def get_target_token_ids(token: str | int | Sequence[int]) -> list[int]:
415 if isinstance(token, str):
416 return cached_encode(tokenizer, token, add_special_tokens=False)
417 if isinstance(token, int):
418 return [token]
419 return list(token)
420
421 updates = []
422 for modality, items in out_mm_kwargs.items():
423 # Popped so they are neither cached nor sent to the model; the updates
424 # they produce are cached alongside the item instead
425 replacements = []
426 for item in items:
427 ids = item.pop(f"{modality}_replacement_ids").data
428 assert isinstance(ids, torch.Tensor)
429 replacements.append(
430 PromptUpdateDetails.select_token_id(
431 ids.tolist(), _get_embed_token_id(ids)
432 )
433 )
434 token = getattr(hf_processor, f"{modality}_token")
435 updates.append(
436 PromptReplacement(
437 modality=modality,
438 target=get_target_token_ids(token),
439 replacement=replacements.__getitem__,
440 )
441 )
442 return updates
443
444 def _get_num_image_patches(
445 self,
446 hf_inputs: "BatchFeature",
447 mm_data: Mapping[str, object],
448 num_images: int,
449 ) -> torch.Tensor:
450 """How many rows of the image fields belong to each image.
451
452 Taken from whichever per-image count the processor reports,
453 and checked against the data it has to slice.
454 """
455 if (grid := hf_inputs.get("image_grid_thw")) is not None:
456 num_patches = grid.prod(-1)
457 elif (counts := self._get_num_patches_per_image(mm_data)) is not None:
458 num_patches = torch.tensor(counts)
459 else:
460 num_patches = torch.ones(num_images, dtype=torch.long)
461
462 image_data = hf_inputs.get("pixel_values", hf_inputs.get("image_patches"))
463 if isinstance(image_data, torch.Tensor):
464 total = int(num_patches.sum())
465 rows = image_data.shape[self._get_slice_dim(image_data, total)]
466 if rows != total:
467 raise ValueError(
468 f"{type(self.info.get_hf_processor()).__name__} returned "
469 f"{rows} row(s) of image data for {num_images} image(s), which "
470 f"cannot be split into the {num_patches.tolist()} row(s) per "
471 "image derived from its outputs, so the rows cannot be "
472 "attributed to an image. Gemma3 does this when "
473 "`do_pan_and_scan` crops an image."
474 )
475 return num_patches
476
477 def _get_num_patches_per_image(
478 self, mm_data: Mapping[str, object]
479 ) -> list[int] | None:
480 """Ask the HF processor how many rows of image data each image produces."""
481 images = mm_data.get("images")
482 if not isinstance(images, Iterable) or not images:
483 return None
484 try:
485 sizes = [(image.height, image.width) for image in images]
486 mm_tokens = self.info.get_hf_processor()._get_num_multimodal_tokens(
487 image_sizes=sizes, **self.info.ctx.get_merged_mm_kwargs({})
488 )
489 return list(mm_tokens["num_image_patches"])
490 except (AttributeError, KeyError, TypeError):
491 return None
492
493 def _apply_hf_processor_main(
494 self,
495 mm_items: MultiModalDataItems,
496 hf_kwargs: Mapping[str, object],
497 ) -> "BatchFeature":
498 hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
499 mm_items, hf_kwargs
500 )
501
502 if not hf_data:
503 return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
504
505 prompt_text = hf_data.pop("text")
506 assert isinstance(prompt_text, str)
507
508 # Ask for the replacement each placeholder expands to, and record it as
509 # per-item fields: its token ids, and the tokens or patches behind them
510 if has_mm_data := any(hf_data.values()):
511 hf_data = {**hf_data, "return_text_replacement_offsets": True}
512
513 try:
514 hf_inputs = self.info.ctx.call_hf_processor(
515 self.info.get_hf_processor(**hf_kwargs),
516 dict(text=prompt_text, **hf_data),
517 hf_kwargs,
518 )
519 except ValueError:
520 if any(hf_data.values()):
521 raise
522 # Some processors reject a prompt holding placeholders with
523 # no data to go with them, so tokenize it without them
524 tokenizer = self.info.get_tokenizer()
525 hf_inputs = transformers.BatchFeature(
526 dict(input_ids=[tokenizer.encode(prompt_text)]), tensor_type="pt"
527 )
528 self._unpad_images(hf_inputs)
529 self._unpad_audios(hf_inputs, hf_data, hf_kwargs)
530
531 # Drop the inputs the model would reject
532 hf_inputs.pop("mm_token_type_ids", None)
533 hf_inputs.pop("token_type_ids", None)
534
535 offsets = hf_inputs.pop("text_replacement_offsets", None)
536 # Some processors return an empty batch as a tensor rather than a list
537 if offsets is None or len(offsets) == 0 or len(offsets[0]) == 0:
538 if has_mm_data:
539 raise ValueError(
540 f"{type(self.info.get_hf_processor()).__name__} returned no "
541 "text replacement offsets, so the Transformers modeling backend "
542 "cannot locate the placeholder of each item. Its `__call__` has "
543 "to reach `ProcessorMixin.get_text_with_replacements` with one "
544 "replacement per item, which usually means implementing "
545 "`replace_<modality>_token`. Please report this to transformers "
546 "so it can be fixed."
547 )
548 hf_inputs.pop("input_ids", None)
549 return self._finalize_hf_mm_data(
550 hf_data, hf_kwargs, passthrough_data, hf_inputs
551 )
552
553 tokenizer = self.info.get_tokenizer()
554 replacements = defaultdict[str, list[list[int]]](list)
555 for entry in offsets[0]:
556 replacements[entry["type"]].append(
557 cached_encode(tokenizer, entry["replacement"], add_special_tokens=False)
558 )
559
560 for modality, seqs in replacements.items():
561 hf_inputs[f"{modality}_replacement_ids"] = torch.tensor(
562 [token_id for seq in seqs for token_id in seq]
563 )
564 hf_inputs[f"{modality}_replacement_sizes"] = torch.tensor(
565 [len(seq) for seq in seqs]
566 )
567 if modality == "image":
568 hf_inputs["num_image_patches"] = self._get_num_image_patches(
569 hf_inputs, hf_data, len(seqs)
570 )
571 elif modality == "audio":
572 counts = []
573 for seq in seqs:
574 ids = torch.tensor(seq)
575 counts.append(int(ids.eq(_get_embed_token_id(ids)).sum()))
576 hf_inputs["num_audio_tokens"] = torch.tensor(counts)
577
578 hf_inputs.pop("input_ids")
579
580 return self._finalize_hf_mm_data(
581 hf_data, hf_kwargs, passthrough_data, hf_inputs
582 )
583
584
585 class MultiModalMixin(SupportsMultiModal, SupportsMRoPE, Base):
586 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
587 # Skip SupportsMRoPE.__init__ and call the next class in MRO
588 super(SupportsMRoPE, self).__init__(vllm_config=vllm_config, prefix=prefix)
589
590 def _find_encoder_classes(
591 self, model: "PreTrainedModel"
592 ) -> dict[str, type["PreTrainedModel"]]:
593 """Modalities whose encoder cannot be told apart from the model itself are
594 omitted, as are those `get_encoder` rejects."""
595 encoder_classes: dict[str, type[PreTrainedModel]] = {}
596 for modality in _MODALITY_TO_TOKEN_TYPE_ID:
597 try:
598 encoder_cls = type(model.get_encoder(modality=modality))
599 except (TypeError, ValueError):
600 continue
601 if encoder_cls is not type(model):
602 encoder_classes[modality] = encoder_cls
603 return encoder_classes
604
605 @contextmanager
606 def _mark_model_components(self, vllm_config: "VllmConfig"):
607 model_config = vllm_config.model_config
608 encoder_classes = self._pre_trained_model_classes.encoders
609 if not encoder_classes:
610 logger.debug("No encoders identified, so no components will be marked")
611 yield
612 return
613
614 if model_config.skip_tokenizer_init:
615 # Determining the supported modalities needs the HF processor, which in
616 # turn needs a tokenizer
617 mm_config = model_config.get_multimodal_config()
618 if mm_config.mm_encoder_only or any(
619 mm_config.get_limit_per_prompt(modality) == 0
620 for modality in encoder_classes
621 ):
622 logger.warning_once(
623 "Unable to determine the supported modalities without a "
624 "tokenizer, so no model components will be skipped."
625 )
626 yield
627 return
628
629 # Modalities we don't serve report a limit of 999, which would stop their
630 # encoder ever being skipped
631 supported_modalities = MULTIMODAL_REGISTRY.get_processing_info(
632 model_config
633 ).supported_mm_limits
634
635 # One encoder often serves several modalities, and may only be skipped when
636 # all of them are disabled, so mark it once for the whole set
637 modalities_by_encoder = defaultdict(set)
638 for modality, encoder_cls in encoder_classes.items():
639 if modality in supported_modalities:
640 modalities_by_encoder[encoder_cls].add(modality)
641
642 with ExitStack() as stack:
643 stack.enter_context(
644 self._mark_language_model(
645 vllm_config, targets=self._pre_trained_model_classes.decoder
646 )
647 )
648 for encoder_cls, modalities in modalities_by_encoder.items():
649 stack.enter_context(
650 self._mark_tower_model(vllm_config, modalities, targets=encoder_cls)
651 )
652 yield
653
654 def _decorate_for_torch_compile(self):
655 """Decorate the model's decoder and encoder classes to indicate to vLLM
656 that they support torch compile if `can_enable_torch_compile` and
657 `should_torch_compile_mm_encoder` are True respectively.
658 """
659 super()._decorate_for_torch_compile()
660 # Decorate the encoder model classes to support torch compile if needed
661 if self.compilation_config.compile_mm_encoder:
662 encoder_classes = self._pre_trained_model_classes.encoders
663 if not encoder_classes:
664 raise ValueError(
665 "Unable to infer any encoder classes from the model. "
666 "You must either: update the model so that "
667 "https://huggingface.co/docs/transformers/en/main_classes/model#transformers.PreTrainedModel.get_encoder"
668 " can detect the encoders correctly, or remove "
669 "'compile_mm_encoder'."
670 )
671 logger.warning_once(
672 "Multimodal encoder compilation with the Transformers modeling backend "
673 "is an experimental feature. It relies on:\n"
674 "- The encoder being torch compilable.\n"
675 "- All encoder tensor inputs must be type hinted as either "
676 "`torch.Tensor` or `torch.FloatTensor`.\n"
677 "- The 0-th dimension of all tensor inputs to the encoder being the "
678 "dynamic dimension (e.g. sequence length, number of patches).\n"
679 "Please report any issues you encounter to help us improve it."
680 )
681 # One encoder can serve several modalities, and must only be decorated once
682 for encoder_cls in dict.fromkeys(encoder_classes.values()):
683 self._decorate_cls_for_torch_compile(
684 cls=encoder_cls,
685 # TODO: properly infer dynamic_arg_dims based on the encoder's
686 # forward method signature. We assume dim 0 for all tensor inputs.
687 dynamic_arg_dims=None,
688 enable_if=should_torch_compile_mm_encoder,
689 is_encoder=True,
690 )
691
692 def forward(
693 self,
694 input_ids: torch.Tensor | None,
695 positions: torch.Tensor,
696 intermediate_tensors: IntermediateTensors | None = None,
697 inputs_embeds: torch.Tensor | None = None,
698 **kwargs: object,
699 ) -> torch.Tensor | IntermediateTensors:
700 # Positions shape handling for MRoPE models
701 if self.model_config.uses_mrope:
702 # [3, seq_len] -> [3, 1, seq_len]
703 positions = positions[:, None].contiguous()
704 model_output = super().forward(
705 input_ids, positions, intermediate_tensors, inputs_embeds
706 )
707 return model_output
708
709 def get_language_model(self) -> torch.nn.Module:
710 """Transformers modeling backend multimodal classes do not contain a separate
711 vLLM language model class. Therefore, in order to return a language model vLLM
712 class, we use a wrapper to give `self` the same interface as a text model."""
713 # Exclude self and object
714 bases = self.__class__.mro()[1:-1]
715 # Keep only classes defined in `vllm.model_executor.models.transformers`
716 bases = [b for b in bases if ".transformers." in b.__module__]
717 # Exclude MultiModalMixin itself
718 bases = [b for b in bases if b is not MultiModalMixin]
719
720 class LanguageModel(*bases): # type: ignore[misc]
721 def __init__(self, multimodal_model):
722 # Don't call super().__init__() to avoid re-initialization
723 self.__dict__.update(multimodal_model.__dict__)
724
725 model = getattr_iter(self.model, ("language_model", "text_model"), None)
726
727 return LanguageModel(self)
728
729 def get_mm_mapping(self) -> MultiModelKeys:
730 """Get the module prefix in multimodal models"""
731 for name in ("language_model", "text_model"):
732 if getattr(self.model, name, None) is not None:
733 return MultiModelKeys.from_string_field(language_model=f"model.{name}")
734 raise ValueError(
735 "Could not locate the language model submodule for LoRA support"
736 )
737
738 def _split_embeddings(
739 self, embeddings: torch.Tensor, split_sizes: list[int]
740 ) -> list[torch.Tensor]:
741 total_expected = sum(split_sizes)
742
743 # Flatten to 2D: [total_tokens, hidden_dim]
744 if embeddings.ndim > 2:
745 embeddings = embeddings.reshape(-1, embeddings.shape[-1])
746
747 total_tokens = embeddings.shape[0]
748 if total_tokens == total_expected:
749 # Direct match: split_sizes are actual token counts
750 token_split_sizes = split_sizes
751 elif total_expected > 0 and total_tokens % total_expected == 0:
752 # Uniform expansion: each item expands to N tokens
753 tokens_per_item = total_tokens // total_expected
754 token_split_sizes = [s * tokens_per_item for s in split_sizes]
755 elif total_expected > 0:
756 # TODO: make this an error once we know profiling never relies on it
757 if total_tokens == 0:
758 raise ValueError(
759 "Encoder returned empty embeddings. "
760 f"Expected {total_expected} tokens from "
761 f"split_sizes={split_sizes}"
762 )
763 # Keep the counts out of the message: `warning_once` keys its cache on
764 # the args, so varying them would log on every new pair
765 logger.warning_once(
766 "Encoder returned a different number of tokens than expected; "
767 "padding or truncating to fit. The embeddings are not trustworthy "
768 "outside of memory profiling."
769 )
770 logger.debug(
771 "Encoder returned %s tokens but %s were expected",
772 total_tokens,
773 total_expected,
774 )
775 if total_tokens < total_expected:
776 repeat_factor = (total_expected + total_tokens - 1) // total_tokens
777 embeddings = embeddings.repeat(repeat_factor, 1)
778 embeddings = embeddings[:total_expected]
779 token_split_sizes = split_sizes
780 else:
781 return []
782
783 return list(torch.split(embeddings, token_split_sizes, dim=0))
784
785 def _process_audio_input(self, **kwargs) -> list[torch.Tensor] | None:
786 input_features: torch.Tensor | None = kwargs.pop("input_features", None)
787 if input_features is None:
788 input_features = kwargs.pop("input_values", None)
789 if input_features is None:
790 return None
791
792 num_audio_tokens = kwargs.pop("num_audio_tokens")
793 kwargs.pop("token_type_ids", None)
794 kwargs.pop("mm_token_type_ids", None)
795
796 # Per-audio token counts are needed as Python ints to split.
797 with gpu_sync_allowed():
798 split_sizes = num_audio_tokens.flatten().tolist()
799 if isinstance(input_features, torch.Tensor):
800 # HuggingFace's `get_audio_features` implementations branch on
801 # per-sample feature lengths internally.
802 with gpu_sync_allowed():
803 audio_output = self.model.get_audio_features(
804 input_features, return_dict=True, **kwargs
805 )
806 return self._split_embeddings(audio_output.pooler_output, split_sizes)
807
808 # Audios the processor left un-padded arrive as a list once their
809 # lengths differ. Encode them one at a time so that none of them is
810 # padded to match another.
811 embeddings: list[torch.Tensor] = []
812 for index, features in enumerate(input_features):
813 audio_output = self.model.get_audio_features(
814 features.unsqueeze(0),
815 return_dict=True,
816 **self._select_item_kwargs(kwargs, index, len(input_features)),
817 )
818 embeddings.extend(
819 self._split_embeddings(audio_output.pooler_output, [split_sizes[index]])
820 )
821 return embeddings
822
823 def _process_image_input(self, **kwargs) -> list[torch.Tensor] | None:
824 pixel_values: torch.Tensor | None = kwargs.pop("pixel_values", None)
825 image_embeds: torch.Tensor | None = kwargs.pop("image_embeds", None)
826 # Model might use `image_patches` instead of `pixel_values`
827 if pixel_values is None:
828 pixel_values = kwargs.pop("image_patches", None)
829
830 if image_embeds is not None:
831 return [image_embeds]
832
833 if pixel_values is None:
834 return None
835
836 num_image_patches = kwargs.pop("num_image_patches")
837
838 split_sizes = num_image_patches.flatten().tolist()
839 if isinstance(pixel_values, torch.Tensor):
840 vision_embeddings = self._get_image_features(pixel_values, **kwargs)
841 if isinstance(vision_embeddings, torch.Tensor):
842 return self._split_embeddings(vision_embeddings, split_sizes)
843 return list(vision_embeddings)
844
845 # Images the processor left un-padded arrive as a list once their
846 # shapes differ. Encode them one at a time so that none of them is
847 # padded to match another.
848 embeddings: list[torch.Tensor] = []
849 for index, image in enumerate(pixel_values):
850 features = self._get_image_features(
851 image.unsqueeze(0),
852 **self._select_item_kwargs(kwargs, index, len(pixel_values)),
853 )
854 # Encoders which return one entry per image return a single entry
855 if not isinstance(features, torch.Tensor):
856 features = torch.cat(list(features))
857 embeddings.extend(self._split_embeddings(features, [split_sizes[index]]))
858 return embeddings
859
860 def _select_item_kwargs(
861 self, kwargs: dict[str, Any], index: int, num_items: int
862 ) -> dict[str, Any]:
863 """Narrow the entries of `kwargs` that hold one row per item down to the item
864 at `index`. Length is all there is to match on, so an unrelated entry of the
865 same length is narrowed too."""
866 return {
867 key: value[index : index + 1]
868 if isinstance(value, (torch.Tensor, list)) and len(value) == num_items
869 else value
870 for key, value in kwargs.items()
871 }
872
873 def _get_image_features(self, pixel_values: torch.Tensor, **kwargs) -> Any:
874 # grid_thw fields are registered keep_on_cpu; restore the on-device
875 # placement that HF get_image_features implementations expect.
876 for key, value in kwargs.items():
877 if isinstance(value, torch.Tensor) and value.is_cpu:
878 kwargs[key] = async_tensor_h2d(value, pixel_values.device)
879
880 # The underlying HuggingFace `get_image_features` implementations
881 # contain model-internal syncs (e.g. Idefics3 filters all-zero
882 # padding images via boolean-mask indexing, LlavaOnevision
883 # branches on per-sample batch counts).
884 with gpu_sync_allowed():
885 features = self.model.get_image_features(pixel_values, **kwargs)
886
887 # Transformers `v5`, `self.get_image_features` returns a tuple
888 # containing the features and optionally attentions/hidden_states
889 # After v5 is settled, we can enable qwen3-vl with several outputs
890 # from `self.get_image_features`
891 if isinstance(features, tuple):
892 return features[0]
893 if isinstance(features, ModelOutput):
894 return features.pooler_output
895 return features
896
897 def embed_multimodal(self, **kwargs) -> MultiModalEmbeddings:
898 # Each helper detects its own inputs. We are called once per modality, so the
899 # leftovers a helper forwards to the HF model can't belong to the other one.
900 embeddings: list[torch.Tensor] = []
901 for process_input in (self._process_audio_input, self._process_image_input):
902 embeddings.extend(process_input(**kwargs) or [])
903 return embeddings
904
905 def get_mrope_input_positions(
906 self,
907 input_tokens: list[int],
908 mm_features: list[MultiModalFeatureSpec],
909 ) -> tuple[torch.Tensor, int]:
910 kwargs = MultiModalFeatureSpec.gather_kwargs(
911 mm_features,
912 {
913 "image_grid_thw",
914 "video_grid_thw",
915 "second_per_grid_ts",
916 "audio_feature_lengths",
917 "use_audio_in_video",
918 },
919 )
920 if any(v for k, v in kwargs.items() if k not in {"image_grid_thw"}):
921 raise NotImplementedError(
922 "Transformers modeling backend only supports images."
923 )
924
925 image_grid_thw = kwargs.get("image_grid_thw", [])
926 video_grid_thw = kwargs.get("video_grid_thw", [])
927
928 image_grid_thw = torch.stack(image_grid_thw) if image_grid_thw else None
929 video_grid_thw = torch.stack(video_grid_thw) if video_grid_thw else None
930
931 # `get_rope_index` doesn't always accept arbitrary `kwargs`
932 if not hasattr(self, "_get_rope_index_kwarg_names"):
933 import inspect
934
935 params = inspect.signature(self.model.get_rope_index).parameters
936 self._get_rope_index_kwarg_names = (
937 None
938 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values())
939 else frozenset(params)
940 )
941
942 kwarg_names = self._get_rope_index_kwarg_names
943
944 def accepts_kwarg(name: str) -> bool:
945 return kwarg_names is None or name in kwarg_names
946
947 # Drop a grid the model can't accept only when there is nothing to pass.
948 kwargs = {
949 name: value
950 for name, value in (
951 ("image_grid_thw", image_grid_thw),
952 ("video_grid_thw", video_grid_thw),
953 )
954 if value is not None or accepts_kwarg(name)
955 }
956 if accepts_kwarg("mm_token_type_ids"):
957 mm_token_type_ids = torch.zeros(len(input_tokens), dtype=torch.int)
958 for feature in mm_features:
959 position = feature.mm_position
960 offset, length = position.offset, position.length
961 mm_token_type_id = _MODALITY_TO_TOKEN_TYPE_ID[feature.modality]
962 mm_token_type_ids[offset : offset + length] = mm_token_type_id
963 kwargs["mm_token_type_ids"] = mm_token_type_ids.unsqueeze(0)
964
965 mrope_positions, mrope_position_delta = self.model.get_rope_index(
966 input_ids=torch.tensor(input_tokens).unsqueeze(0),
967 **kwargs,
968 )
969
970 mrope_positions = mrope_positions[:, 0]
971 mrope_position_delta = mrope_position_delta[0].item()
972
973 return mrope_positions, mrope_position_delta