[MM] Further cleanup _apply_hf_processor_main (#53610)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>

Cyrus Leung committed Sep 17, 2026 at 01:21 UTC 0b9e018fb941652441e5d661ef5b8397e4909ebd
74 files changed +1056 -1331
docs/contributing/model/multimodal.md
+22 -11
@@ -454,17 +454,17 @@ return a schema of the tensors outputted by the HF processor that are related to
454 ??? code
455
456 ```python
457 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
457 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
458 # Mistral3Processor requires text corresponding to the images
459 return self.dummy_inputs.get_dummy_text(mm_counts)
460
461 def _postprocess_hf_mm_data(
462 self,
463 - mm_data: Mapping[str, object],
464 - hf_processor_mm_kwargs: Mapping[str, object],
463 + hf_data: Mapping[str, object],
464 + hf_kwargs: Mapping[str, object],
465 processed_data: BatchFeature,
466 ) -> BatchFeature:
467 - if not mm_data:
467 + if not hf_data:
468 return processed_data
469
470 pixel_values = processed_data.get("pixel_values")
@@ -486,25 +486,36 @@ return a schema of the tensors outputted by the HF processor that are related to
486 calls the HF processor on the multi-modal data without passing any text.
487 If the HF processor instead requires text corresponding to the multi-modal items,
488 you should override
489 - [_get_hf_processor_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_processor_text]
489 + [_get_hf_mm_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_text]
490 to return the dummy text from
491 [BaseDummyInputsBuilder.get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text]
492 like in the example above. If the HF processor expects the multi-modal data
493 - under different keys than those provided by the multi-modal items
494 - (e.g. `audio` instead of `audios`), or requires additional keyword arguments
493 + under different keys than those provided by the multi-modal items,
494 + or requires additional keyword arguments
495 (e.g. `sampling_rate`), you should override
496 - [_preprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._preprocess_hf_mm_data].
496 + [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs].
497 If you need to modify the output of the HF processor,
498 you should override
499 [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data].
500 + Passthrough fields have already been merged into `processed_data` when this
501 + hook is called.
502 + If you need to change how the normalized inputs are passed to the HF
503 + processor (for example, to call a method other than `__call__`, or to use
504 + different kwargs to construct and invoke it), override
505 + [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor].
506 If the HF processor applies additional transformations to the prompt tokens
507 that are not reflected in its multi-modal outputs (e.g. prepending a BOS token),
508 you should override
509 [_postprocess_prompt][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_prompt]
510 to replicate them.
505 - For even more control over how the HF processor is called, you can override
511 + Only processors that require multiple HF calls or manually construct the
512 + complete output should override
513 [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]
507 - directly.
514 + directly. Such implementations must obtain their normalized inputs through
515 + [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs]
516 + and return through
517 + [_finalize_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._finalize_hf_mm_data]
518 + so passthrough fields and model-specific postprocessing are applied consistently.
519
520 Since `pixel_values` is now a list with one tensor per image, we can override
521 [_get_mm_fields_config][vllm.multimodal.processing.BaseMultiModalProcessor._get_mm_fields_config] as follows:
@@ -724,7 +735,7 @@ Examples:
735
736 ### Custom HF processor
737
727 -Some models don't define an HF processor class on HF Hub. In that case, you can define a custom HF processor that has the same call signature as HF processors and return it from [BaseProcessingInfo.get_hf_processor][vllm.multimodal.processing.BaseProcessingInfo.get_hf_processor]. It is then applied to the multi-modal data via [InputProcessingContext.call_hf_processor][vllm.multimodal.processing.InputProcessingContext.call_hf_processor] inside [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main].
738 +Some models don't define an HF processor class on HF Hub. In that case, you can define a custom HF processor that has the same call signature as HF processors and return it from [BaseProcessingInfo.get_hf_processor][vllm.multimodal.processing.BaseProcessingInfo.get_hf_processor]. The default [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor] applies it to the normalized multi-modal data via [InputProcessingContext.call_hf_processor][vllm.multimodal.processing.InputProcessingContext.call_hf_processor]. Override `_call_hf_processor` if the custom processor instead exposes a different callable interface.
739
740 Examples:
741
docs/design/mm_processing.md
+9 -2
@@ -8,9 +8,16 @@ In vLLM's rendering pipeline (see [BaseRenderer][vllm.renderers.base.BaseRendere
8
9 Since Transformers 5.10, `ProcessorMixin` now allows multi-modal inputs to be passed by themselves. However, certain subclasses (such as `ChameleonProcessor`) and older out-of-tree implementations may still define their own `__call__` method that assumes the presence of text with corresponding placeholder tokens. This causes a problem as we don't have the original text anymore to pass to these processors.
10
11 -To work around this, each model defines how to generate dummy text based on the number of multi-modal inputs, via [get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text], which its override of [_get_hf_processor_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_processor_text] returns so that [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main] passes it to the HF processor together with the multi-modal inputs to obtain the processed multi-modal data.
11 +To work around this, each model defines how to generate dummy text based on the number of multi-modal inputs, via [get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text], which its override of [_get_hf_mm_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_text] returns. [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs] adds this text and `truncation=False` to the normalized HF inputs. The truncation setting is attached only to calls that use dummy text, so unrelated processor calls do not receive a text-specific option.
12
13 -Similarly, since the multi-modal data extracted by vLLM may not match what a specific HF processor expects, [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main] allows each model to adapt the inputs via [_preprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._preprocess_hf_mm_data] (e.g. renaming keys like `audios` to `audio` or injecting extra keyword arguments such as `sampling_rate`) and the outputs via [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data], without having to reimplement the entire method.
13 +HF processing is split into four overridable stages:
14 +
15 +1. [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs] extracts and normalizes processor data, kwargs, and passthrough data. It centrally renames vLLM's `audios` field to the standard HF `audio` keyword; processors that require another spelling can adapt it in an override.
16 +2. [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor] invokes the HF processor. Models can override it when they use a different callable or need separate construction and invocation kwargs.
17 +3. [_finalize_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._finalize_hf_mm_data] merges passthrough fields into the processed output. It lazily creates an empty output when there is no processor data.
18 +4. [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data] performs model-specific output transformations after passthrough fields have been merged.
19 +
20 +This division lets most models customize one stage without reimplementing [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]. Implementations that genuinely require multiple processor calls or manually construct their complete output may still override that method, but must use `_get_hf_mm_inputs` and `_finalize_hf_mm_data` to retain the common input and output behavior.
21
22 ## Prompt Update Detection
23
vllm/model_executor/models/audioflamingo3.py
+1 -13
@@ -374,21 +374,9 @@ class AudioFlamingo3MultiModalDataParser(MultiModalDataParser):
374 class AudioFlamingo3MultiModalProcessor(
375 BaseMultiModalProcessor[AudioFlamingo3ProcessingInfo]
376 ):
377 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
377 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
378 return self.dummy_inputs.get_dummy_text(mm_counts)
379
380 - def _preprocess_hf_mm_data(
381 - self,
382 - mm_data: Mapping[str, object],
383 - hf_processor_mm_kwargs: Mapping[str, object],
384 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
385 - mm_data = dict(mm_data)
386 - audios = mm_data.pop("audios", None)
387 - if audios is not None:
388 - mm_data["audio"] = audios
389 -
390 - return mm_data, hf_processor_mm_kwargs
391 -
380 def _postprocess_hf_mm_data(
381 self,
382 mm_data: Mapping[str, object],
vllm/model_executor/models/bailing_moe_v3_vl.py
+1 -1
@@ -119,7 +119,7 @@ class BailingMoeV3VLDummyInputsBuilder(
119 class BailingMoeV3VLMultiModalProcessor(
120 BaseMultiModalProcessor[BailingMoeV3VLProcessingInfo]
121 ):
122 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
122 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
123 return self.dummy_inputs.get_dummy_text(mm_counts)
124
125 def _apply_prompt_updates(
vllm/model_executor/models/cohere2_vision.py
+1 -1
@@ -222,7 +222,7 @@ class Cohere2VisionDummyInputsBuilder(
222 class Cohere2VisionMultiModalProcessor(
223 BaseMultiModalProcessor[Cohere2VisionProcessingInfo]
224 ):
225 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
225 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
226 return self.dummy_inputs.get_dummy_text(mm_counts)
227
228 def _postprocess_hf_mm_data(
vllm/model_executor/models/cohere_asr.py
+19 -29
@@ -8,7 +8,7 @@ from typing import Any, ClassVar
8 import torch
9 import torch.nn.functional as F
10 from torch import nn
11 -from transformers import BatchFeature, PretrainedConfig
11 +from transformers import PretrainedConfig
12
13 from vllm.compilation.decorators import support_torch_compile
14 from vllm.config import CacheConfig, ModelConfig, SpeechToTextConfig, VllmConfig
@@ -50,7 +50,10 @@ from vllm.multimodal.processing import (
50 PromptUpdate,
51 TimingContext,
52 )
53 -from vllm.multimodal.processing.processor import MultiModalProcessingInfo
53 +from vllm.multimodal.processing.processor import (
54 + HFMultiModalInputs,
55 + MultiModalProcessingResult,
56 +)
57 from vllm.renderers import TokenizeParams
58 from vllm.tokenizers import cached_tokenizer_from_config
59 from vllm.transformers_utils.processors.cohere_asr import (
@@ -1962,42 +1965,29 @@ class CohereASRMultiModalProcessor(EncDecMultiModalProcessor[CohereASRProcessing
1965 ) -> list[int]:
1966 return [0]
1967
1965 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
1968 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1969 return self.dummy_inputs.get_dummy_text(mm_counts)
1970
1968 - def _preprocess_hf_mm_data(
1971 + def _get_hf_mm_inputs(
1972 self,
1970 - mm_data: Mapping[str, object],
1971 - hf_processor_mm_kwargs: Mapping[str, object],
1972 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1973 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
1974 -
1975 - mm_data = dict(mm_data)
1976 - mm_data["audio"] = mm_data.pop("audios")
1977 -
1978 - hf_processor_mm_kwargs = dict(
1979 - **hf_processor_mm_kwargs,
1980 - sampling_rate=feature_extractor.sampling_rate,
1973 + mm_items: MultiModalDataItems,
1974 + hf_kwargs: Mapping[str, object],
1975 + ) -> HFMultiModalInputs:
1976 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
1977 +
1978 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
1979 + return hf_inputs._replace(
1980 + hf_kwargs=dict(
1981 + hf_inputs.hf_kwargs,
1982 + sampling_rate=feature_extractor.sampling_rate,
1983 + )
1984 )
1985
1983 - return mm_data, hf_processor_mm_kwargs
1984 -
1985 - def _postprocess_hf_mm_data(
1986 - self,
1987 - mm_data: Mapping[str, object],
1988 - hf_processor_mm_kwargs: Mapping[str, object],
1989 - processed_data: BatchFeature,
1990 - ) -> BatchFeature:
1991 - if "labels" in processed_data:
1992 - processed_data["input_ids"] = processed_data.pop("labels")
1993 -
1994 - return processed_data
1995 -
1986 def _cached_apply_hf_processor(
1987 self,
1988 inputs: ProcessorInputs,
1989 timing_ctx: TimingContext,
2000 - ) -> MultiModalProcessingInfo:
1990 + ) -> MultiModalProcessingResult:
1991 # Dithering injects noise into the extracted features, so the
1992 # feature extractor is not a pure function of its input. Since the
1993 # processing cache assumes that processor outputs are invariant
vllm/model_executor/models/colmodernvbert.py
+7 -7
@@ -157,15 +157,14 @@ class ColModernVBertMultiModalProcessor(
157 def _apply_hf_processor_main(
158 self,
159 mm_items: MultiModalDataItems,
160 - hf_processor_mm_kwargs: Mapping[str, object],
160 + hf_kwargs: Mapping[str, object],
161 ) -> BatchFeature:
162 - valid_mm_items = mm_items.select(
163 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
162 + mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
163 + mm_items, hf_kwargs
164 )
165 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
165
166 if not mm_data:
168 - return BatchFeature(dict(passthrough_data))
167 + return self._finalize_hf_mm_data(mm_data, hf_kwargs, passthrough_data)
168
169 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
170
@@ -193,8 +192,9 @@ class ColModernVBertMultiModalProcessor(
192 result.update(image_outputs)
193
194 processed_data = result
196 - processed_data.update(passthrough_data)
197 - return processed_data
195 + return self._finalize_hf_mm_data(
196 + mm_data, hf_kwargs, passthrough_data, processed_data
197 + )
198
199 def _get_mm_fields_config(
200 self,
vllm/model_executor/models/colpali.py
+7 -18
@@ -20,7 +20,7 @@ from collections.abc import Iterable, Mapping
20
21 import torch
22 import torch.nn as nn
23 -from transformers import BatchFeature, PaliGemmaProcessor
23 +from transformers import PaliGemmaProcessor
24
25 from vllm.config import VllmConfig
26 from vllm.model_executor.layers.pooler.tokwise import pooler_for_token_embed
@@ -28,6 +28,7 @@ from vllm.model_executor.layers.pooler.tokwise.heads import TokenPoolerHead
28 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
29 from vllm.multimodal import MULTIMODAL_REGISTRY
30 from vllm.multimodal.parse import MultiModalDataItems
31 +from vllm.multimodal.processing.processor import HFMultiModalInputs
32
33 from .interfaces import SupportsLateInteraction
34 from .interfaces_base import default_pooling_type
@@ -59,31 +60,19 @@ class ColPaliProcessingInfo(PaliGemmaProcessingInfo):
60 class ColPaliMultiModalProcessor(PaliGemmaMultiModalProcessor):
61 """Multimodal processor for ColPali."""
62
62 - def _apply_hf_processor_main(
63 + def _get_hf_mm_inputs(
64 self,
65 mm_items: MultiModalDataItems,
65 - hf_processor_mm_kwargs: Mapping[str, object],
66 - ) -> BatchFeature:
67 - valid_mm_items = mm_items.select(
68 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
69 - )
70 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
71 -
72 - if not mm_data:
73 - return BatchFeature(dict(passthrough_data))
66 + hf_kwargs: Mapping[str, object],
67 + ) -> HFMultiModalInputs:
68 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
69
70 # The ColPali tokenizer_config.json ships with a small default
71 # max_length (50) that truncates the 1024 image tokens inserted
72 # by PaliGemmaProcessor, causing a token-count mismatch.
73 # vLLM enforces its own max_model_len, so we disable HF
74 # truncation to keep all image + text tokens intact.
80 - processed_data = self.info.ctx.call_hf_processor(
81 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
82 - mm_data,
83 - dict(**hf_processor_mm_kwargs, truncation=False),
84 - )
85 - processed_data.update(passthrough_data)
86 - return processed_data
75 + return hf_inputs._replace(hf_kwargs=dict(hf_inputs.hf_kwargs, truncation=False))
76
77
78 @default_pooling_type(seq_pooling_type="CLS", tok_pooling_type="ALL")
vllm/model_executor/models/deepseek_ocr.py
+11 -26
@@ -46,10 +46,10 @@ from vllm.multimodal.processing import (
46 PromptReplacement,
47 PromptUpdate,
48 )
49 +from vllm.multimodal.processing.processor import HFMultiModalInputs
50 from vllm.sampling_params import SamplingParams
51 from vllm.sequence import IntermediateTensors
52 from vllm.tokenizers import cached_tokenizer_from_config
52 -from vllm.tokenizers.hf import HfTokenizer
53 from vllm.transformers_utils.configs.deepseek_vl2 import DeepseekVLV2Config
54 from vllm.transformers_utils.processors.deepseek_ocr import (
55 BASE_SIZE,
@@ -287,34 +287,19 @@ class DeepseekOCRDummyInputsBuilder(BaseDummyInputsBuilder[DeepseekOCRProcessing
287 class DeepseekOCRMultiModalProcessor(
288 BaseMultiModalProcessor[DeepseekOCRProcessingInfo]
289 ):
290 - def _apply_hf_processor_main(
290 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
291 + return self.dummy_inputs.get_dummy_text(mm_counts)
292 +
293 + def _get_hf_mm_inputs(
294 self,
295 mm_items: MultiModalDataItems,
293 - hf_processor_mm_kwargs: Mapping[str, object],
294 - ) -> BatchFeature:
295 - valid_mm_items = mm_items.select(
296 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
297 - )
298 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
299 -
300 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
301 -
302 - if mm_data:
303 - processed_data = self.info.ctx.call_hf_processor(
304 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
305 - dict(prompt=prompt_text, **mm_data),
306 - hf_processor_mm_kwargs,
307 - )
308 -
309 - else:
310 - tokenizer = self.info.get_tokenizer()
311 - assert isinstance(tokenizer, HfTokenizer)
312 - processed_data = tokenizer(
313 - prompt_text, add_special_tokens=True, return_tensors="pt"
314 - )
296 + hf_kwargs: Mapping[str, object],
297 + ) -> HFMultiModalInputs:
298 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
299 + if "text" in hf_inputs.hf_data:
300 + hf_inputs.hf_data["prompt"] = hf_inputs.hf_data.pop("text")
301
316 - processed_data.update(passthrough_data)
317 - return processed_data
302 + return hf_inputs
303
304 def _get_mm_fields_config(
305 self,
vllm/model_executor/models/deepseek_ocr2.py
+11 -27
@@ -45,9 +45,9 @@ from vllm.multimodal.processing import (
45 PromptReplacement,
46 PromptUpdate,
47 )
48 +from vllm.multimodal.processing.processor import HFMultiModalInputs
49 from vllm.sequence import IntermediateTensors
50 from vllm.tokenizers import cached_tokenizer_from_config
50 -from vllm.tokenizers.hf import HfTokenizer
51 from vllm.transformers_utils.configs.deepseek_vl2 import DeepseekVLV2Config
52 from vllm.transformers_utils.processors.deepseek_ocr import (
53 BASE_SIZE,
@@ -158,35 +158,19 @@ class DeepseekOCR2DummyInputsBuilder(
158 class DeepseekOCR2MultiModalProcessor(
159 BaseMultiModalProcessor[DeepseekOCR2ProcessingInfo]
160 ):
161 - def _apply_hf_processor_main(
161 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
162 + return self.dummy_inputs.get_dummy_text(mm_counts)
163 +
164 + def _get_hf_mm_inputs(
165 self,
166 mm_items: MultiModalDataItems,
164 - hf_processor_mm_kwargs: Mapping[str, object],
165 - ) -> BatchFeature:
166 - valid_mm_items = mm_items.select(
167 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
168 - )
169 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
170 -
171 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
172 -
173 - if mm_data:
174 - processed_data = self.info.ctx.call_hf_processor(
175 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
176 - dict(prompt=prompt_text, **mm_data),
177 - hf_processor_mm_kwargs,
178 - )
179 -
180 - else:
181 - tokenizer = self.info.get_tokenizer()
182 - assert isinstance(tokenizer, HfTokenizer)
183 - processed_data = tokenizer(
184 - prompt_text, add_special_tokens=True, return_tensors="pt"
185 - )
186 -
187 - processed_data.update(passthrough_data)
167 + hf_kwargs: Mapping[str, object],
168 + ) -> HFMultiModalInputs:
169 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
170 + if "text" in hf_inputs.hf_data:
171 + hf_inputs.hf_data["prompt"] = hf_inputs.hf_data.pop("text")
172
189 - return processed_data
173 + return hf_inputs
174
175 def _get_mm_fields_config(
176 self,
vllm/model_executor/models/deepseek_vl2.py
+3 -3
@@ -35,7 +35,7 @@ from vllm.multimodal.processing import BaseDummyInputsBuilder
35 from vllm.multimodal.processing.processor import (
36 BaseMultiModalProcessor,
37 BaseProcessingInfo,
38 - MultiModalProcessingInfo,
38 + MultiModalProcessingResult,
39 ProcessorInputs,
40 PromptReplacement,
41 PromptUpdate,
@@ -233,7 +233,7 @@ class DeepseekVL2DummyInputsBuilder(BaseDummyInputsBuilder[DeepseekVL2Processing
233 class DeepseekVL2MultiModalProcessor(
234 BaseMultiModalProcessor[DeepseekVL2ProcessingInfo]
235 ):
236 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
236 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
237 return self.dummy_inputs.get_dummy_text(mm_counts)
238
239 def _postprocess_hf_mm_data(
@@ -307,7 +307,7 @@ class DeepseekVL2MultiModalProcessor(
307 self,
308 inputs: ProcessorInputs,
309 timing_ctx: TimingContext,
310 - ) -> MultiModalProcessingInfo:
310 + ) -> MultiModalProcessingResult:
311 # The processor logic is different for len(images) <= 2 vs > 2
312 # Since the processing cache assumes that the processor output is
313 # invariant of how many images are passed per prompt, we only
vllm/model_executor/models/ernie45_vl.py
+50 -57
@@ -71,6 +71,7 @@ from vllm.multimodal.processing import (
71 PromptUpdate,
72 cached_encode,
73 )
74 +from vllm.multimodal.processing.processor import HFMultiModalInputs
75 from vllm.sequence import IntermediateTensors
76 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
77 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -1112,34 +1113,32 @@ class Ernie4_5VLMultiModalProcessor(BaseMultiModalProcessor[Ernie4_5_VLProcessin
1113 pixel_values = pixel_values.to(hf_config.dtype)
1114 return pixel_values
1115
1115 - def _apply_hf_processor_main(
1116 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1117 + return self.dummy_inputs.get_dummy_text(mm_counts)
1118 +
1119 + def _get_hf_mm_inputs(
1120 self,
1121 mm_items: MultiModalDataItems,
1118 - hf_processor_mm_kwargs: Mapping[str, object],
1119 - ) -> BatchFeature:
1120 - valid_mm_items = mm_items.select(
1121 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1122 - )
1123 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1124 -
1125 - if "images" not in mm_data and "videos" not in mm_data:
1126 - return BatchFeature(dict(passthrough_data))
1122 + hf_kwargs: Mapping[str, object],
1123 + ) -> HFMultiModalInputs:
1124 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
1125 + hf_data = hf_inputs.hf_data
1126
1128 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
1127 + if "images" not in hf_data and "videos" not in hf_data:
1128 + return hf_inputs
1129
1130 - mm_data = dict(mm_data)
1131 - if "images" not in mm_data:
1132 - mm_data["images"] = []
1133 - if "videos" not in mm_data:
1134 - mm_data["videos"] = []
1130 + prompt_text = hf_data["text"]
1131 + hf_data["text"] = [prompt_text]
1132 + hf_data.setdefault("images", [])
1133 + hf_data.setdefault("videos", [])
1134
1135 # Check if HF processor supports video metadata
1137 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1136 + hf_processor = self.info.get_hf_processor(**hf_inputs.hf_kwargs)
1137 supports_video_metadata = getattr(
1138 hf_processor, "supports_video_metadata", False
1139 )
1140
1142 - videos = mm_data["videos"]
1141 + videos = hf_data["videos"]
1142 assert isinstance(videos, Sequence)
1143 if videos and not supports_video_metadata:
1144 # Old HF processor, unwrap tuple to pure frames
@@ -1147,49 +1146,43 @@ class Ernie4_5VLMultiModalProcessor(BaseMultiModalProcessor[Ernie4_5_VLProcessin
1146 "HF processor doesn't support video metadata. "
1147 "Timestamps will NOT be rendered. Please upgrade the model."
1148 )
1150 - mm_data["videos"] = [v[0] if isinstance(v, tuple) else v for v in videos]
1149 + hf_data["videos"] = [v[0] if isinstance(v, tuple) else v for v in videos]
1150
1152 - processor_output = self.info.ctx.call_hf_processor(
1153 - hf_processor,
1154 - dict(
1155 - text=[prompt_text], images=mm_data["images"], videos=mm_data["videos"]
1156 - ),
1157 - hf_processor_mm_kwargs,
1158 - )
1151 + return hf_inputs
1152 +
1153 + def _postprocess_hf_mm_data(
1154 + self,
1155 + hf_data: Mapping[str, object],
1156 + hf_kwargs: Mapping[str, object],
1157 + processed_data: BatchFeature,
1158 + ) -> BatchFeature:
1159 + if "images" not in hf_data and "videos" not in hf_data:
1160 + return processed_data
1161
1162 # Divide the processor_output into two modalities: image and video.
1161 - if processor_output is not None:
1162 - pixel_values = processor_output["images"]
1163 - if pixel_values is not None:
1164 - processor_output["images"] = self._pixel_values_norm(
1165 - pixel_values, hf_processor_mm_kwargs
1166 - )
1167 - for key in list(processor_output.keys()):
1168 - if processor_output[key] is None:
1169 - del processor_output[key]
1170 - continue
1171 - if key == "grid_thw":
1172 - grid_thw = processor_output["grid_thw"]
1173 - pixel_values_all = processor_output["images"]
1174 - # Identify elements where the first
1175 - # dimension is greater than 1 and
1176 - # treat them as the video modality
1177 - mask = grid_thw[:, 0] > 1
1178 - processor_output["video_grid_thw"] = grid_thw[mask]
1179 - processor_output["image_grid_thw"] = grid_thw[~mask]
1180 - image_patch_num = (
1181 - processor_output["image_grid_thw"].prod(dim=1).sum()
1182 - )
1183 - processor_output["pixel_values"] = pixel_values_all[
1184 - :image_patch_num
1185 - ]
1186 - processor_output["pixel_values_videos"] = pixel_values_all[
1187 - image_patch_num:
1188 - ]
1189 - del processor_output["images"]
1163 + pixel_values = processed_data["images"]
1164 + if pixel_values is not None:
1165 + processed_data["images"] = self._pixel_values_norm(pixel_values, hf_kwargs)
1166 + for key in list(processed_data.keys()):
1167 + if processed_data[key] is None:
1168 + del processed_data[key]
1169 + continue
1170 + if key == "grid_thw":
1171 + grid_thw = processed_data["grid_thw"]
1172 + pixel_values_all = processed_data["images"]
1173 + # Identify elements where the first
1174 + # dimension is greater than 1 and
1175 + # treat them as the video modality
1176 + mask = grid_thw[:, 0] > 1
1177 + processed_data["video_grid_thw"] = grid_thw[mask]
1178 + processed_data["image_grid_thw"] = grid_thw[~mask]
1179 + image_patch_num = processed_data["image_grid_thw"].prod(dim=1).sum()
1180 + processed_data["pixel_values"] = pixel_values_all[:image_patch_num]
1181 + processed_data["pixel_values_videos"] = pixel_values_all[
1182 + image_patch_num:
1183 + ]
1184 + del processed_data["images"]
1185
1191 - processed_data = processor_output
1192 - processed_data.update(passthrough_data)
1186 return processed_data
1187
1188 def _get_prompt_updates(
vllm/model_executor/models/fireredasr2.py
+14 -26
@@ -38,6 +38,7 @@ from vllm.multimodal.processing import (
38 PromptUpdate,
39 PromptUpdateDetails,
40 )
41 +from vllm.multimodal.processing.processor import HFMultiModalInputs
42 from vllm.transformers_utils.processor import cached_processor_from_config
43 from vllm.transformers_utils.processors.fireredasr2 import (
44 FireRedASR2FeatureExtractor,
@@ -228,37 +229,24 @@ class FireRedASR2DummyInputsBuilder(BaseDummyInputsBuilder[FireRedASR2Processing
229 class FireRedASR2MultiModalProcessor(
230 BaseMultiModalProcessor[FireRedASR2ProcessingInfo]
231 ):
231 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
232 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
233 return self.dummy_inputs.get_dummy_text(mm_counts)
234
234 - def _preprocess_hf_mm_data(
235 + def _get_hf_mm_inputs(
236 self,
236 - mm_data: Mapping[str, object],
237 - hf_processor_mm_kwargs: Mapping[str, object],
238 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
239 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
240 -
241 - mm_data = dict(mm_data)
242 - mm_data["audio"] = mm_data.pop("audios")
243 -
244 - hf_processor_mm_kwargs = dict(
245 - **hf_processor_mm_kwargs,
246 - sampling_rate=feature_extractor.sampling_rate,
237 + mm_items: MultiModalDataItems,
238 + hf_kwargs: Mapping[str, object],
239 + ) -> HFMultiModalInputs:
240 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
241 +
242 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
243 + return hf_inputs._replace(
244 + hf_kwargs=dict(
245 + hf_inputs.hf_kwargs,
246 + sampling_rate=feature_extractor.sampling_rate,
247 + )
248 )
249
249 - return mm_data, hf_processor_mm_kwargs
250 -
251 - def _postprocess_hf_mm_data(
252 - self,
253 - mm_data: Mapping[str, object],
254 - hf_processor_mm_kwargs: Mapping[str, object],
255 - processed_data: BatchFeature,
256 - ) -> BatchFeature:
257 - if "labels" in processed_data:
258 - processed_data["input_ids"] = processed_data.pop("labels")
259 -
260 - return processed_data
261 -
250 def _get_mm_fields_config(
251 self,
252 hf_inputs: BatchFeature,
vllm/model_executor/models/funasr.py
+18 -28
@@ -49,7 +49,10 @@ from vllm.multimodal.processing import (
49 PromptUpdate,
50 TimingContext,
51 )
52 -from vllm.multimodal.processing.processor import MultiModalProcessingInfo
52 +from vllm.multimodal.processing.processor import (
53 + HFMultiModalInputs,
54 + MultiModalProcessingResult,
55 +)
56 from vllm.transformers_utils.processor import cached_processor_from_config
57 from vllm.transformers_utils.processors.funasr import FunASRFeatureExtractor
58 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -752,42 +755,29 @@ class FunASRDummyInputsBuilder(BaseDummyInputsBuilder[FunASRProcessingInfo]):
755
756
757 class FunASRMultiModalProcessor(BaseMultiModalProcessor[FunASRProcessingInfo]):
755 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
758 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
759 return self.dummy_inputs.get_dummy_text(mm_counts)
760
758 - def _preprocess_hf_mm_data(
761 + def _get_hf_mm_inputs(
762 self,
760 - mm_data: Mapping[str, object],
761 - hf_processor_mm_kwargs: Mapping[str, object],
762 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
763 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
764 -
765 - mm_data = dict(mm_data)
766 - mm_data["audio"] = mm_data.pop("audios")
767 -
768 - hf_processor_mm_kwargs = dict(
769 - **hf_processor_mm_kwargs,
770 - sampling_rate=feature_extractor.sampling_rate,
763 + mm_items: MultiModalDataItems,
764 + hf_kwargs: Mapping[str, object],
765 + ) -> HFMultiModalInputs:
766 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
767 +
768 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
769 + return hf_inputs._replace(
770 + hf_kwargs=dict(
771 + hf_inputs.hf_kwargs,
772 + sampling_rate=feature_extractor.sampling_rate,
773 + )
774 )
775
773 - return mm_data, hf_processor_mm_kwargs
774 -
775 - def _postprocess_hf_mm_data(
776 - self,
777 - mm_data: Mapping[str, object],
778 - hf_processor_mm_kwargs: Mapping[str, object],
779 - processed_data: BatchFeature,
780 - ) -> BatchFeature:
781 - if "labels" in processed_data:
782 - processed_data["input_ids"] = processed_data.pop("labels")
783 -
784 - return processed_data
785 -
776 def _cached_apply_hf_processor(
777 self,
778 inputs: ProcessorInputs,
779 timing_ctx: TimingContext,
790 - ) -> MultiModalProcessingInfo:
780 + ) -> MultiModalProcessingResult:
781 # Dithering injects noise into the extracted features, so the
782 # feature extractor is not a pure function of its input. Since the
783 # processing cache assumes that processor outputs are invariant
vllm/model_executor/models/funaudiochat.py
+12 -11
@@ -616,29 +616,29 @@ class FunAudioChatMultiModalProcessor(
616 def _apply_hf_processor_main(
617 self,
618 mm_items: MultiModalDataItems,
619 - hf_processor_mm_kwargs: Mapping[str, object],
619 + hf_kwargs: Mapping[str, object],
620 ) -> BatchFeature:
621 - valid_mm_items = mm_items.select(
622 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
621 + mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
622 + mm_items, hf_kwargs
623 )
624 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
624
625 if not mm_data:
627 - return BatchFeature(dict(passthrough_data))
626 + return self._finalize_hf_mm_data(mm_data, hf_kwargs, passthrough_data)
627
628 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
629
630 tokenizer = self.info.get_tokenizer()
631 input_ids = torch.tensor([tokenizer.encode(prompt_text)])
632
634 - audios = mm_data.get("audios", [])
633 + audios = mm_data.get("audio", [])
634 if not audios:
635 processed_data = BatchFeature({"input_ids": input_ids})
637 - processed_data.update(passthrough_data)
638 - return processed_data
636 + return self._finalize_hf_mm_data(
637 + mm_data, hf_kwargs, passthrough_data, processed_data
638 + )
639 assert isinstance(audios, Sequence)
640
641 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
641 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
642 sr = int(feature_extractor.sampling_rate)
643 min_samples = int(getattr(feature_extractor, "n_fft", 400) or 400)
644
@@ -690,8 +690,9 @@ class FunAudioChatMultiModalProcessor(
690 }
691
692 processed_data = BatchFeature({"input_ids": input_ids, **mm_inputs})
693 - processed_data.update(passthrough_data)
694 - return processed_data
693 + return self._finalize_hf_mm_data(
694 + mm_data, hf_kwargs, passthrough_data, processed_data
695 + )
696
697 def _get_mm_fields_config(
698 self,
vllm/model_executor/models/gemma3_mm.py
+1 -1
@@ -263,7 +263,7 @@ class Gemma3DummyInputsBuilder(BaseDummyInputsBuilder[Gemma3ProcessingInfo]):
263
264
265 class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
266 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
266 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
267 return self.dummy_inputs.get_dummy_text(mm_counts)
268
269 def _postprocess_hf_mm_data(
vllm/model_executor/models/gemma3n_mm.py
+1 -12
@@ -265,20 +265,9 @@ class Gemma3nDummyInputsBuilder(BaseDummyInputsBuilder[Gemma3nProcessingInfo]):
265
266
267 class Gemma3nMultiModalProcessor(BaseMultiModalProcessor[Gemma3nProcessingInfo]):
268 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
268 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
269 return self.dummy_inputs.get_dummy_text(mm_counts)
270
271 - def _preprocess_hf_mm_data(
272 - self,
273 - mm_data: Mapping[str, object],
274 - hf_processor_mm_kwargs: Mapping[str, object],
275 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
276 - mm_data = dict(mm_data)
277 - if "audios" in mm_data:
278 - mm_data["audio"] = mm_data.pop("audios")
279 -
280 - return mm_data, hf_processor_mm_kwargs
281 -
271 def _postprocess_hf_mm_data(
272 self,
273 mm_data: Mapping[str, object],
vllm/model_executor/models/gemma4_mm.py
+20 -23
@@ -595,22 +595,25 @@ class Gemma4DummyInputsBuilder(BaseDummyInputsBuilder[Gemma4ProcessingInfo]):
595
596
597 class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
598 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
599 + return self.dummy_inputs.get_dummy_text(mm_counts)
600 +
601 def _apply_hf_processor_main(
602 self,
603 mm_items: MultiModalDataItems,
601 - hf_processor_mm_kwargs: Mapping[str, object],
604 + hf_kwargs: Mapping[str, object],
605 ) -> BatchFeature:
603 - valid_mm_items = mm_items.select(
604 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
606 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
607 + mm_items, hf_kwargs
608 )
606 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
609
608 - if not mm_data:
609 - return BatchFeature(dict(passthrough_data))
610 + if not hf_data:
611 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
612
611 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
613 + prompt_text = hf_data.pop("text")
614 + assert isinstance(prompt_text, str)
615
613 - merged_kwargs = self.info.ctx.get_merged_mm_kwargs(hf_processor_mm_kwargs)
616 + merged_kwargs = self.info.ctx.get_merged_mm_kwargs(hf_kwargs)
617 val, is_top_level_max_soft_tokens = _get_max_soft_tokens(merged_kwargs)
618
619 if val is not None and val not in _SUPPORTED_SOFT_TOKENS:
@@ -619,14 +622,12 @@ class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
622 f"Valid values are {_SUPPORTED_SOFT_TOKENS}."
623 )
624
622 - mm_data = dict(mm_data)
623 -
625 # ---- VIDEO HANDLING ----
626 # Gemma4 decomposes video into timestamped image frames.
627 # Each frame is processed with max_soft_tokens=70 through the
628 # same vision tower, matching transformers processing_gemma4.py.
629 video_outputs: dict[str, Any] = {}
629 - if videos := mm_data.pop("videos", []):
630 + if videos := hf_data.pop("videos", []):
631 assert isinstance(videos, list)
632 processor = self.info.get_hf_processor()
633
@@ -656,7 +657,7 @@ class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
657 timestamps = [idx / fps for idx in frame_indices]
658
659 # Process frames as images with max_soft_tokens=70
659 - video_mm_kwargs = dict(hf_processor_mm_kwargs)
660 + video_mm_kwargs = dict(hf_kwargs)
661 # Override configured image options too: these inputs are video
662 # frames, whose cache keys exclude images_kwargs.
663 video_mm_kwargs["images_kwargs"] = {}
@@ -726,18 +727,14 @@ class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
727 "video_timestamps": video_timestamps_per_video,
728 }
729
729 - # The processor accepts 'audio' not 'audios'.
730 - if "audios" in mm_data:
731 - mm_data["audio"] = mm_data.pop("audios")
732 -
730 # Warn if any audio waveform exceeds the model's max duration.
734 - if "audio" in mm_data:
731 + if "audio" in hf_data:
732 processor = self.info.get_hf_processor()
733 sr = processor.feature_extractor.sampling_rate
734 max_tokens = processor.audio_seq_length
735 ms_per_tok = processor.audio_ms_per_token
736 max_duration_s = max_tokens * ms_per_tok / 1000.0
740 - audios = mm_data["audio"]
737 + audios = hf_data["audio"]
738 if not isinstance(audios, (list, tuple)):
739 audios = [audios]
740 for i, waveform in enumerate(audios):
@@ -760,13 +757,13 @@ class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
757 # NOTE: This requires a corresponding type annotation on the
758 # HF side (Gemma4ProcessorKwargs.images_kwargs) so that
759 # _merge_kwargs routes max_soft_tokens into images_kwargs.
763 - patched_mm_kwargs = dict(hf_processor_mm_kwargs)
760 + patched_mm_kwargs = dict(hf_kwargs)
761 if val is not None and is_top_level_max_soft_tokens:
762 patched_mm_kwargs["max_soft_tokens"] = val
763
764 processed_data = self.info.ctx.call_hf_processor(
765 self.info.get_hf_processor(**patched_mm_kwargs),
769 - dict(text=prompt_text, **mm_data),
766 + dict(text=prompt_text, **hf_data),
767 patched_mm_kwargs,
768 )
769
@@ -797,9 +794,9 @@ class Gemma4MultiModalProcessor(BaseMultiModalProcessor[Gemma4ProcessingInfo]):
794 processed_data["input_features_mask"] = unpadded_masks
795
796 processed_data.update(video_outputs)
800 - processed_data.update(passthrough_data)
801 -
802 - return processed_data
797 + return self._finalize_hf_mm_data(
798 + hf_data, hf_kwargs, passthrough_data, processed_data
799 + )
800
801 def _get_mm_fields_config(
802 self,
vllm/model_executor/models/glm4_1v.py
+30 -23
@@ -1582,52 +1582,59 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1582
1583 return prepared_data, prepared_kwargs
1584
1585 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1586 + return self.dummy_inputs.get_dummy_text(mm_counts)
1587 +
1588 def _apply_hf_processor_main(
1589 self,
1590 mm_items: MultiModalDataItems,
1588 - hf_processor_mm_kwargs: Mapping[str, object],
1591 + hf_kwargs: Mapping[str, object],
1592 ) -> BatchFeature:
1590 - valid_mm_items = mm_items.select(
1591 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1593 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1594 + mm_items, hf_kwargs
1595 )
1593 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1594 -
1595 - if not mm_data:
1596 - return BatchFeature(dict(passthrough_data))
1596
1598 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
1597 + if not hf_data:
1598 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1599
1600 - mm_data = dict(mm_data)
1600 + prompt_text = hf_data.pop("text")
1601 + assert isinstance(prompt_text, str)
1602
1602 - processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1603 + processor = self.info.get_hf_processor(**hf_kwargs)
1604
1605 use_direct_path = (
1606 not isinstance(processor, Glm4vProcessor) and TRANSFORMERS_WITH_GA
1607 )
1608 if use_direct_path:
1609 prepared_data, prepared_kwargs = self._get_direct_path_inputs(
1609 - mm_data, hf_processor_mm_kwargs
1610 + hf_data, hf_kwargs
1611 )
1612 processed_data = self.info.ctx.call_hf_processor(
1613 self.info.get_hf_processor(**prepared_kwargs),
1614 dict(text=prompt_text, **prepared_data),
1615 prepared_kwargs,
1616 )
1616 - processed_data.update(passthrough_data)
1617 - return processed_data
1617 + return self._finalize_hf_mm_data(
1618 + hf_data, hf_kwargs, passthrough_data, processed_data
1619 + )
1620
1619 - videos = mm_data.get("videos")
1620 - if isinstance(videos, list) and videos:
1621 + if (
1622 + "videos" in hf_data
1623 + and isinstance(videos := hf_data["videos"], list)
1624 + and len(videos) > 0
1625 + ):
1626 + # Videos are processed item-wise below; remove them from hf_data so
1627 + # the combined call at the end does not re-sample the raw frames.
1628 + del hf_data["videos"]
1629 video_grid_thw_lst = []
1630 pixel_values_videos_lst = []
1631 frame_embed_token_id = self.info._get_video_frame_embed_token_id(processor)
1632 swap_video_frame_tokens = frame_embed_token_id == processor.image_token_id
1625 - mm_data.pop("videos")
1633 for item in videos:
1634 video_array, metadata = item
1635
1636 # don't update mm_kwargs inplace
1630 - video_mm_kwargs = dict(**hf_processor_mm_kwargs)
1637 + video_mm_kwargs = dict(**hf_kwargs)
1638 video_mm_kwargs["do_sample_frames"] = metadata.get(
1639 "do_sample_frames", True
1640 )
@@ -1667,9 +1674,9 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1674 swap_video_frame_tokens = False
1675
1676 processed_data = self.info.ctx.call_hf_processor(
1670 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
1671 - dict(text=prompt_text, **mm_data),
1672 - hf_processor_mm_kwargs,
1677 + self.info.get_hf_processor(**hf_kwargs),
1678 + dict(text=prompt_text, **hf_data),
1679 + hf_kwargs,
1680 )
1681 if swap_video_frame_tokens:
1682 input_ids = processed_data["input_ids"]
@@ -1677,9 +1684,9 @@ class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1684 processed_data["input_ids"] = input_ids
1685
1686 processed_data.update(video_outputs)
1680 - processed_data.update(passthrough_data)
1681 -
1682 - return processed_data
1687 + return self._finalize_hf_mm_data(
1688 + hf_data, hf_kwargs, passthrough_data, processed_data
1689 + )
1690
1691 def _get_mm_fields_config(
1692 self,
vllm/model_executor/models/glmasr.py
+14 -15
@@ -44,6 +44,7 @@ from vllm.multimodal.processing import (
44 PromptUpdate,
45 PromptUpdateDetails,
46 )
47 +from vllm.multimodal.processing.processor import HFMultiModalInputs
48 from vllm.sequence import IntermediateTensors
49 from vllm.tokenizers import cached_tokenizer_from_config
50 from vllm.transformers_utils.processor import cached_processor_from_config
@@ -739,26 +740,24 @@ class GlmAsrMultiModalProcessor(BaseMultiModalProcessor["GlmAsrProcessingInfo"])
740 chunk_counts.append(min(n_chunks, max_windows))
741 return chunk_counts
742
742 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
743 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
744 return self.dummy_inputs.get_dummy_text(mm_counts)
745
745 - def _preprocess_hf_mm_data(
746 + def _get_hf_mm_inputs(
747 self,
747 - mm_data: Mapping[str, object],
748 - hf_processor_mm_kwargs: Mapping[str, object],
749 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
750 - mm_data = dict(mm_data)
751 - if "audios" in mm_data:
752 - mm_data["audio"] = mm_data.pop("audios")
753 -
754 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
755 - hf_processor_mm_kwargs = dict(
756 - **hf_processor_mm_kwargs,
757 - sampling_rate=feature_extractor.sampling_rate,
748 + mm_items: MultiModalDataItems,
749 + hf_kwargs: Mapping[str, object],
750 + ) -> HFMultiModalInputs:
751 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
752 +
753 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
754 + return hf_inputs._replace(
755 + hf_kwargs=dict(
756 + hf_inputs.hf_kwargs,
757 + sampling_rate=feature_extractor.sampling_rate,
758 + )
759 )
760
760 - return mm_data, hf_processor_mm_kwargs
761 -
761 def _postprocess_hf_mm_data(
762 self,
763 mm_data: Mapping[str, object],
vllm/model_executor/models/granite_speech.py
+5 -19
@@ -110,7 +110,7 @@ class GraniteSpeechAudioInputs(TensorSchema):
110 """List of audio embedding sizes for each item in batch."""
111
112
113 -class GraniteSpeechMultiModalProcessingInfo(BaseProcessingInfo):
113 +class GraniteSpeechProcessingInfo(BaseProcessingInfo):
114 def get_data_parser(self):
115 feature_extractor = self.get_hf_processor().audio_processor
116
@@ -135,7 +135,7 @@ class GraniteSpeechMultiModalProcessingInfo(BaseProcessingInfo):
135
136 ### Input Processing & Multimodal utils
137 class GraniteSpeechMultiModalProcessor(
138 - BaseMultiModalProcessor[GraniteSpeechMultiModalProcessingInfo]
138 + BaseMultiModalProcessor[GraniteSpeechProcessingInfo]
139 ):
140 def _get_mm_fields_config(
141 self,
@@ -181,23 +181,9 @@ class GraniteSpeechMultiModalProcessor(
181 )
182 ]
183
184 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
184 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
185 return self.dummy_inputs.get_dummy_text(mm_counts)
186
187 - def _preprocess_hf_mm_data(
188 - self,
189 - mm_data: Mapping[str, object],
190 - hf_processor_mm_kwargs: Mapping[str, object],
191 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
192 - mm_data = dict(mm_data)
193 - audios = mm_data.pop("audios", [])
194 -
195 - if audios:
196 - # GraniteSpeechFeatureExtractor accepts "audio"
197 - mm_data["audio"] = audios
198 -
199 - return mm_data, hf_processor_mm_kwargs
200 -
187 def _postprocess_hf_mm_data(
188 self,
189 mm_data: Mapping[str, object],
@@ -216,7 +202,7 @@ class GraniteSpeechMultiModalProcessor(
202
203
204 class GraniteSpeechDummyInputsBuilder(
219 - BaseDummyInputsBuilder[GraniteSpeechMultiModalProcessingInfo]
205 + BaseDummyInputsBuilder[GraniteSpeechProcessingInfo]
206 ):
207 def get_dummy_mm_data(
208 self,
@@ -565,7 +551,7 @@ class GraniteSpeechCTCEncoder(nn.Module):
551
552 @MULTIMODAL_REGISTRY.register_processor(
553 GraniteSpeechMultiModalProcessor,
568 - info=GraniteSpeechMultiModalProcessingInfo,
554 + info=GraniteSpeechProcessingInfo,
555 dummy_inputs=GraniteSpeechDummyInputsBuilder,
556 )
557 class GraniteSpeechForConditionalGeneration(
vllm/model_executor/models/granite_speech_plus.py
+2 -2
@@ -12,8 +12,8 @@ from .granite_speech import (
12 GraniteSpeechCTCEncoder,
13 GraniteSpeechDummyInputsBuilder,
14 GraniteSpeechForConditionalGeneration,
15 - GraniteSpeechMultiModalProcessingInfo,
15 GraniteSpeechMultiModalProcessor,
16 + GraniteSpeechProcessingInfo,
17 )
18
19 ISO639_1_SUPPORTED_LANGS = {
@@ -60,7 +60,7 @@ class GraniteSpeechPlusCTCEncoder(GraniteSpeechCTCEncoder):
60
61 @MULTIMODAL_REGISTRY.register_processor(
62 GraniteSpeechMultiModalProcessor,
63 - info=GraniteSpeechMultiModalProcessingInfo,
63 + info=GraniteSpeechProcessingInfo,
64 dummy_inputs=GraniteSpeechDummyInputsBuilder,
65 )
66 class GraniteSpeechPlusForConditionalGeneration(GraniteSpeechForConditionalGeneration):
vllm/model_executor/models/h2ovl.py
+2 -2
@@ -21,7 +21,7 @@ from vllm.multimodal.parse import (
21 MultiModalDataItems,
22 )
23 from vllm.multimodal.processing.processor import (
24 - MultiModalProcessingInfo,
24 + MultiModalProcessingResult,
25 ProcessorInputs,
26 PromptReplacement,
27 TimingContext,
@@ -139,7 +139,7 @@ class H2OVLMultiModalProcessor(BaseInternVLMultiModalProcessor[H2OVLProcessingIn
139 self,
140 inputs: ProcessorInputs,
141 timing_ctx: TimingContext,
142 - ) -> MultiModalProcessingInfo:
142 + ) -> MultiModalProcessingResult:
143 # The processor logic is different for len(images) <= 1 vs > 1
144 # Since the processing cache assumes that the processor output is
145 # invariant of how many images are passed per prompt, we only
vllm/model_executor/models/hyperclovax_vision_v2.py
+2 -38
@@ -261,44 +261,8 @@ class HCXVisionV2MultiModalProcessor(
261 ):
262 """Multimodal processor for HyperCLOVAX V2 (32B Think model)."""
263
264 - def _apply_hf_processor_main(
265 - self,
266 - mm_items: MultiModalDataItems,
267 - hf_processor_mm_kwargs: Mapping[str, object],
268 - ) -> BatchFeature:
269 - valid_mm_items = mm_items.select(
270 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
271 - )
272 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
273 -
274 - if not mm_data:
275 - return BatchFeature(dict(passthrough_data))
276 -
277 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
278 -
279 - images = mm_data.get("images")
280 - videos = mm_data.get("videos")
281 -
282 - # Get the HF processor
283 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
284 -
285 - # Build data dict for HF processor (images/videos only)
286 - # NOTE: We pass the prompt as-is without token normalization.
287 - # Token expansion is handled by vLLM via _get_prompt_updates.
288 - data: dict[str, object] = dict(
289 - text=prompt_text,
290 - images=images,
291 - videos=videos,
292 - )
293 -
294 - processed_data = self.info.ctx.call_hf_processor(
295 - hf_processor=hf_processor,
296 - data=data,
297 - kwargs=hf_processor_mm_kwargs,
298 - )
299 - processed_data.update(passthrough_data)
300 -
301 - return processed_data
264 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
265 + return self.dummy_inputs.get_dummy_text(mm_counts)
266
267 def _get_prompt_updates(
268 self,
vllm/model_executor/models/idefics3.py
+21 -21
@@ -52,6 +52,7 @@ from vllm.multimodal.processing import (
52 PromptUpdateDetails,
53 cached_encode,
54 )
55 +from vllm.multimodal.processing.processor import HFMultiModalInputs
56 from vllm.sequence import IntermediateTensors
57 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
58 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -240,31 +241,32 @@ class Idefics3DummyInputsBuilder(BaseDummyInputsBuilder[Idefics3ProcessingInfo])
241
242
243 class Idefics3MultiModalProcessor(BaseMultiModalProcessor[Idefics3ProcessingInfo]):
243 - def _apply_hf_processor_main(
244 + def _get_hf_mm_inputs(
245 self,
246 mm_items: MultiModalDataItems,
246 - hf_processor_mm_kwargs: Mapping[str, object],
247 - ) -> BatchFeature:
248 - valid_mm_items = mm_items.select(
249 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
250 - )
251 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
252 -
253 - if not mm_data:
254 - return BatchFeature(dict(passthrough_data))
247 + hf_kwargs: Mapping[str, object],
248 + ) -> HFMultiModalInputs:
249 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
250
251 image_processor = self.info.get_hf_processor().image_processor
252 if getattr(image_processor, "backend", "pil") == "pil":
258 - hf_processor_mm_kwargs = {
259 - "input_data_format": "channels_last",
260 - **hf_processor_mm_kwargs,
261 - }
253 + hf_inputs = hf_inputs._replace(
254 + hf_kwargs={
255 + "input_data_format": "channels_last",
256 + **hf_inputs.hf_kwargs,
257 + }
258 + )
259
263 - processed_data = self.info.ctx.call_hf_processor(
264 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
265 - mm_data,
266 - hf_processor_mm_kwargs,
267 - )
260 + return hf_inputs
261 +
262 + def _postprocess_hf_mm_data(
263 + self,
264 + mm_data: Mapping[str, object],
265 + hf_processor_mm_kwargs: Mapping[str, object],
266 + processed_data: BatchFeature,
267 + ) -> BatchFeature:
268 + if not mm_data:
269 + return processed_data
270
271 images = mm_data.get("images", [])
272 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
@@ -289,8 +291,6 @@ class Idefics3MultiModalProcessor(BaseMultiModalProcessor[Idefics3ProcessingInfo
291 processed_data["pixel_values"].squeeze_(0)
292 processed_data["pixel_attention_mask"].squeeze_(0)
293
292 - processed_data.update(passthrough_data)
293 -
294 return processed_data
295
296 def _get_mm_fields_config(
vllm/model_executor/models/interns1.py
+20 -18
@@ -335,28 +335,30 @@ class InternS1DummyInputsBuilder(BaseDummyInputsBuilder[InternS1ProcessingInfo])
335 class InternS1MultiModalProcessor(BaseMultiModalProcessor[InternS1ProcessingInfo]):
336 """Basic image-only MultiModalProcessor for InternS1-style models."""
337
338 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
339 + return self.dummy_inputs.get_dummy_text(mm_counts)
340 +
341 def _apply_hf_processor_main(
342 self,
343 mm_items: MultiModalDataItems,
341 - hf_processor_mm_kwargs: Mapping[str, object],
344 + hf_kwargs: Mapping[str, object],
345 ) -> BatchFeature:
343 - valid_mm_items = mm_items.select(
344 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
346 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
347 + mm_items, hf_kwargs
348 )
346 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
349
348 - if not mm_data:
349 - return BatchFeature(dict(passthrough_data))
350 + if not hf_data:
351 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
352
351 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
353 + prompt_text = hf_data.pop("text")
354 + assert isinstance(prompt_text, str)
355
353 - mm_data = dict(mm_data)
354 - videos = mm_data.pop("videos", [])
355 - images = mm_data.pop("images", [])
356 + videos = hf_data.pop("videos", [])
357 + images = hf_data.pop("images", [])
358 assert isinstance(videos, list)
359 assert isinstance(images, list)
360
359 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
361 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
362 tokenizer = hf_processor.tokenizer
363 vocab = tokenizer.get_vocab()
364
@@ -374,9 +376,9 @@ class InternS1MultiModalProcessor(BaseMultiModalProcessor[InternS1ProcessingInfo
376 image_pixel_values = []
377 for image in images:
378 processed_data = self.info.ctx.call_hf_processor(
377 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
379 + self.info.get_hf_processor(**hf_kwargs),
380 dict(text=hf_processor.image_token, **{"images": image}),
379 - hf_processor_mm_kwargs,
381 + hf_kwargs,
382 )
383 image_pixel_values.append(processed_data.pop("pixel_values"))
384
@@ -398,9 +400,9 @@ class InternS1MultiModalProcessor(BaseMultiModalProcessor[InternS1ProcessingInfo
400 video_pixel_values = []
401 for video in videos:
402 processed_data = self.info.ctx.call_hf_processor(
401 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
403 + self.info.get_hf_processor(**hf_kwargs),
404 dict(text=hf_processor.video_token, **{"videos": video}),
403 - hf_processor_mm_kwargs,
405 + hf_kwargs,
406 )
407 video_pixel_values.append(processed_data.pop("pixel_values"))
408
@@ -430,9 +432,9 @@ class InternS1MultiModalProcessor(BaseMultiModalProcessor[InternS1ProcessingInfo
432 processed_data.update(text_outputs)
433 processed_data.update(image_outputs)
434 processed_data.update(video_outputs)
433 - processed_data.update(passthrough_data)
434 -
435 - return processed_data
435 + return self._finalize_hf_mm_data(
436 + hf_data, hf_kwargs, passthrough_data, processed_data
437 + )
438
439 def _get_mm_fields_config(
440 self,
vllm/model_executor/models/jina_vl.py
+8 -7
@@ -13,7 +13,7 @@ from vllm.model_executor.layers.pooler import DispatchPooler
13 from vllm.multimodal import MULTIMODAL_REGISTRY
14 from vllm.multimodal.inputs import MultiModalKwargsItems
15 from vllm.multimodal.processing.processor import (
16 - MultiModalProcessingInfo,
16 + MultiModalProcessingResult,
17 ProcessorInputs,
18 TimingContext,
19 )
@@ -63,19 +63,19 @@ class JinaVLMultiModalProcessor(Qwen2VLMultiModalProcessor):
63 self,
64 inputs: ProcessorInputs,
65 timing_ctx: TimingContext,
66 - ) -> MultiModalProcessingInfo:
67 - mm_info = super()._cached_apply_hf_processor(inputs, timing_ctx)
66 + ) -> MultiModalProcessingResult:
67 + mm_res = super()._cached_apply_hf_processor(inputs, timing_ctx)
68
69 # Score inputs are query-first, while the prompt template is document-first.
70 mm_kwargs = MultiModalKwargsItems(
71 {
72 modality: list(reversed(items))
73 - for modality, items in mm_info.kwargs.items()
73 + for modality, items in mm_res.kwargs.items()
74 }
75 )
76 mm_hashes = {
77 modality: list(reversed(hashes))
78 - for modality, hashes in mm_info.hashes.items()
78 + for modality, hashes in mm_res.hashes.items()
79 }
80 mm_prompt_updates = {
81 modality: [
@@ -85,10 +85,11 @@ class JinaVLMultiModalProcessor(Qwen2VLMultiModalProcessor):
85 ]
86 for item_idx, updates in enumerate(reversed(item_updates))
87 ]
88 - for modality, item_updates in mm_info.prompt_updates.items()
88 + for modality, item_updates in mm_res.prompt_updates.items()
89 }
90
91 - return MultiModalProcessingInfo(
91 + return MultiModalProcessingResult(
92 + prompt_ids=mm_res.prompt_ids,
93 kwargs=mm_kwargs,
94 hashes=mm_hashes,
95 prompt_updates=mm_prompt_updates,
vllm/model_executor/models/kanana_v.py
+14 -10
@@ -459,24 +459,27 @@ class KananaVMultiModalProcessor(BaseMultiModalProcessor[KananaVProcessingInfo])
459 def media_token_id(self) -> int:
460 return self.info.get_hf_config().text_config.eos_token_id + 1
461
462 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
463 + return self.dummy_inputs.get_dummy_text(mm_counts)
464 +
465 def _apply_hf_processor_main(
466 self,
467 mm_items: MultiModalDataItems,
465 - hf_processor_mm_kwargs: Mapping[str, object],
468 + hf_kwargs: Mapping[str, object],
469 ) -> BatchFeature:
470 """Run the underlying HF processor on text and image data."""
468 - valid_mm_items = mm_items.select(
469 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
471 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
472 + mm_items, hf_kwargs
473 )
471 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
474
473 - if not mm_data or not mm_data.get("images", []):
474 - return BatchFeature(dict(passthrough_data))
475 + if not hf_data or not hf_data.get("images", []):
476 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
477
476 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
478 + prompt_text = hf_data.pop("text")
479 + assert isinstance(prompt_text, str)
480
481 # Images
479 - image_inputs = mm_data.get("images", [])
482 + image_inputs = hf_data.get("images", [])
483 pixel_sizes = []
484 if not isinstance(image_inputs[0], Image.Image):
485 image_inputs = [Image.fromarray(image) for image in image_inputs]
@@ -530,8 +533,9 @@ class KananaVMultiModalProcessor(BaseMultiModalProcessor[KananaVProcessingInfo])
533 pixel_sizes=torch.tensor(pixel_sizes),
534 )
535 processed_data = BatchFeature(combined_outputs, tensor_type="pt")
533 - processed_data.update(passthrough_data)
534 - return processed_data
536 + return self._finalize_hf_mm_data(
537 + hf_data, hf_kwargs, passthrough_data, processed_data
538 + )
539
540 def _get_prompt_updates(
541 self,
vllm/model_executor/models/keye.py
+1 -1
@@ -1160,7 +1160,7 @@ class KeyeDummyInputsBuilder(KeyeBaseDummyInputsBuilder[KeyeProcessingInfo]):
1160
1161
1162 class KeyeMultiModalProcessor(BaseMultiModalProcessor[KeyeProcessingInfo]):
1163 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
1163 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1164 return self.dummy_inputs.get_dummy_text(mm_counts)
1165
1166 def _get_prompt_updates(
vllm/model_executor/models/keye_vl1_5.py
+1 -1
@@ -373,7 +373,7 @@ class KeyeVL1_5ProcessingInfo(KeyeProcessingInfo):
373
374
375 class KeyeVL1_5MultiModalProcessor(BaseMultiModalProcessor[KeyeVL1_5ProcessingInfo]):
376 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
376 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
377 return self.dummy_inputs.get_dummy_text(mm_counts)
378
379 def _get_prompt_updates(
vllm/model_executor/models/kimi_audio.py
+8 -24
@@ -47,6 +47,7 @@ from vllm.multimodal.processing import (
47 )
48 from vllm.multimodal.processing.processor import (
49 BaseMultiModalProcessor,
50 + HFMultiModalInputs,
51 ProcessorInputs,
52 )
53 from vllm.sequence import IntermediateTensors
@@ -227,27 +228,17 @@ class KimiAudioMultiModalDataParser(MultiModalDataParser):
228 class KimiAudioMultiModalProcessor(BaseMultiModalProcessor[KimiAudioProcessingInfo]):
229 """vLLM multi-modal processor wrapper for Kimi-Audio."""
230
230 - def _apply_hf_processor_main(
231 + def _get_hf_mm_inputs(
232 self,
233 mm_items: MultiModalDataItems,
233 - hf_processor_mm_kwargs: Mapping[str, object],
234 - ) -> BatchFeature:
235 - """Call the HuggingFace processor."""
236 - valid_mm_items = mm_items.select(
237 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
238 - )
239 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
240 -
241 - if not mm_data:
242 - return BatchFeature(dict(passthrough_data))
243 -
244 - # Convert mm_data format: {'audios': [...]} -> {'audio': ...}
245 - mm_data = dict(mm_data)
246 - audios = mm_data.pop("audios", [])
234 + hf_kwargs: Mapping[str, object],
235 + ) -> HFMultiModalInputs:
236 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
237
238 # Convert audio format: [(array, sr), ...] -> [array, ...]
239 # KimiAudioProcessor expects raw numpy arrays
250 - if audios:
240 + mm_data = hf_inputs.hf_data
241 + if audios := mm_data.pop("audio", []):
242 audio_arrays = []
243 for aud in audios:
244 if isinstance(aud, (tuple, list)) and len(aud) == 2:
@@ -259,14 +250,7 @@ class KimiAudioMultiModalProcessor(BaseMultiModalProcessor[KimiAudioProcessingIn
250 audio_arrays.append(aud)
251 mm_data["audio"] = audio_arrays
252
262 - # Use the context's call_hf_processor for proper handling
263 - processed_data = self.info.ctx.call_hf_processor(
264 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
265 - mm_data,
266 - hf_processor_mm_kwargs,
267 - )
268 - processed_data.update(passthrough_data)
269 - return processed_data
253 + return hf_inputs
254
255 def _get_mm_fields_config(
256 self,
vllm/model_executor/models/kimi_vl.py
+1 -1
@@ -236,7 +236,7 @@ class KimiVLDummyInputsBuilder(BaseDummyInputsBuilder[KimiVLProcessingInfo]):
236
237
238 class KimiVLMultiModalProcessor(BaseMultiModalProcessor[KimiVLProcessingInfo]):
239 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
239 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
240 return self.dummy_inputs.get_dummy_text(mm_counts)
241
242 def _get_mm_fields_config(
vllm/model_executor/models/lfm2_vl.py
+1 -1
@@ -406,7 +406,7 @@ class Lfm2VLDummyInputsBuilder(BaseDummyInputsBuilder[Lfm2VLProcessingInfo]):
406
407
408 class Lfm2VLMultiModalProcessor(BaseMultiModalProcessor[Lfm2VLProcessingInfo]):
409 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
409 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
410 return self.dummy_inputs.get_dummy_text(mm_counts)
411
412 def _postprocess_hf_mm_data(
vllm/model_executor/models/lightonocr.py
+2 -26
@@ -1,7 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Iterable, Mapping, Sequence
4 -from typing import TypeVar
4
5 import torch
6 import torch.nn as nn
@@ -35,8 +34,6 @@ from vllm.multimodal.processing import (
34 PromptUpdateDetails,
35 )
36
38 -_I = TypeVar("_I", bound=Mistral3ProcessingInfo)
39 -
37
38 class LightOnOCRProcessingInfo(Mistral3ProcessingInfo):
39 def get_vision_encoder_info(
@@ -49,8 +46,8 @@ class LightOnOCRProcessingInfo(Mistral3ProcessingInfo):
46 return PixtralHFEncoderInfo(self.get_hf_config())
47
48
52 -class LightOnOCRMultiModalProcessor(BaseMultiModalProcessor[Mistral3ProcessingInfo]):
53 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
49 +class LightOnOCRMultiModalProcessor(BaseMultiModalProcessor[LightOnOCRProcessingInfo]):
50 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
51 return self.dummy_inputs.get_dummy_text(mm_counts)
52
53 def _postprocess_hf_mm_data(
@@ -62,27 +59,6 @@ class LightOnOCRMultiModalProcessor(BaseMultiModalProcessor[Mistral3ProcessingIn
59 if not mm_data:
60 return processed_data
61
65 - input_ids = processed_data.get("input_ids")
66 - if input_ids is not None:
67 - processor = self.info.get_hf_processor()
68 - tokenizer = self.info.get_tokenizer()
69 - vocab = tokenizer.get_vocab()
70 -
71 - break_id = vocab.get(processor.image_break_token)
72 - end_id = vocab.get(processor.image_end_token)
73 -
74 - # create mask to remove break/end tokens
75 - keep_mask = ~torch.isin(
76 - input_ids,
77 - torch.tensor([break_id, end_id]),
78 - )
79 -
80 - processed_data["input_ids"] = input_ids[keep_mask].unsqueeze(0)
81 - if "attention_mask" in processed_data:
82 - processed_data["attention_mask"] = processed_data["attention_mask"][
83 - keep_mask
84 - ].unsqueeze(0)
85 -
62 # un-pad pixel_values per-image so caches remain independent.
63 pixel_values = processed_data.get("pixel_values")
64 if pixel_values is not None:
vllm/model_executor/models/llava.py
+1 -1
@@ -320,7 +320,7 @@ class PixtralHFProcessingInfo(BaseLlavaProcessingInfo):
320
321
322 class PixtralHFMultiModalProcessor(BaseMultiModalProcessor[PixtralHFProcessingInfo]):
323 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
323 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
324 return self.dummy_inputs.get_dummy_text(mm_counts)
325
326 def _postprocess_hf_mm_data(
vllm/model_executor/models/llava_next.py
+1 -1
@@ -209,7 +209,7 @@ class BaseLlavaNextMultiModalProcessor(BaseLlavaMultiModalProcessor[_I]):
209
210
211 class LlavaNextMultiModalProcessor(BaseLlavaNextMultiModalProcessor[_I]):
212 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
212 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
213 return self.dummy_inputs.get_dummy_text(mm_counts)
214
215 def _get_mm_fields_config(
vllm/model_executor/models/llava_onevision.py
+25 -21
@@ -324,33 +324,36 @@ class LlavaOnevisionMultiModalProcessor(
324 pixel_values_videos=MultiModalFieldConfig.batched("video"),
325 )
326
327 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
328 + return self.dummy_inputs.get_dummy_text(mm_counts)
329 +
330 def _apply_hf_processor_main(
331 self,
332 mm_items: MultiModalDataItems,
330 - hf_processor_mm_kwargs: Mapping[str, object],
333 + hf_kwargs: Mapping[str, object],
334 ) -> BatchFeature:
332 - valid_mm_items = mm_items.select(
333 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
335 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
336 + mm_items, hf_kwargs
337 )
335 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
338
337 - if not mm_data:
338 - return BatchFeature(dict(passthrough_data))
339 + if not hf_data:
340 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
341
340 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
342 + prompt_text = hf_data.pop("text")
343 + assert isinstance(prompt_text, str)
344
342 - mm_data = dict(mm_data)
343 - videos = mm_data.pop("videos", [])
345 + videos = hf_data.pop("videos", [])
346 assert isinstance(videos, list)
347
348 if not videos:
349 processed_data = self.info.ctx.call_hf_processor(
348 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
349 - dict(text=prompt_text, **mm_data),
350 - hf_processor_mm_kwargs,
350 + self.info.get_hf_processor(**hf_kwargs),
351 + dict(text=prompt_text, **hf_data),
352 + hf_kwargs,
353 + )
354 + return self._finalize_hf_mm_data(
355 + hf_data, hf_kwargs, passthrough_data, processed_data
356 )
352 - processed_data.update(passthrough_data)
353 - return processed_data
357
358 # LLaVA-OneVision processor doesn't support multiple videos
359 # with different sizes when converting back to tensors
@@ -360,13 +363,13 @@ class LlavaOnevisionMultiModalProcessor(
363 image_token = processor.image_token
364 video_token = processor.video_token
365
363 - images = mm_data.pop("images", [])
366 + images = hf_data.pop("images", [])
367 assert isinstance(images, list)
368 if images:
369 processor_outputs = self.info.ctx.call_hf_processor(
367 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
370 + self.info.get_hf_processor(**hf_kwargs),
371 dict(text=image_token * len(images), **{"images": images}),
369 - hf_processor_mm_kwargs,
372 + hf_kwargs,
373 )
374 image_outputs = {
375 k: v
@@ -379,9 +382,9 @@ class LlavaOnevisionMultiModalProcessor(
382 pixel_values_videos = []
383 for video in videos:
384 item_outputs = self.info.ctx.call_hf_processor(
382 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
385 + self.info.get_hf_processor(**hf_kwargs),
386 dict(text=video_token, **{"videos": video}),
384 - hf_processor_mm_kwargs,
387 + hf_kwargs,
388 )
389
390 pixel_values_videos.append(item_outputs["pixel_values_videos"][0])
@@ -394,8 +397,9 @@ class LlavaOnevisionMultiModalProcessor(
397 **video_outputs,
398 )
399 processed_data = BatchFeature(combined_outputs)
397 - processed_data.update(passthrough_data)
398 - return processed_data
400 + return self._finalize_hf_mm_data(
401 + hf_data, hf_kwargs, passthrough_data, processed_data
402 + )
403
404 def _get_prompt_updates(
405 self,
vllm/model_executor/models/llava_onevision2.py
+37 -33
@@ -1556,23 +1556,26 @@ class LlavaOnevision2MultiModalDataParser(MultiModalDataParser):
1556 class LlavaOnevision2MultiModalProcessor(
1557 BaseMultiModalProcessor[LlavaOnevision2ProcessingInfo]
1558 ):
1559 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1560 + return self.dummy_inputs.get_dummy_text(mm_counts)
1561 +
1562 def _apply_hf_processor_main(
1563 self,
1564 mm_items: MultiModalDataItems,
1562 - hf_processor_mm_kwargs: Mapping[str, object],
1565 + hf_kwargs: Mapping[str, object],
1566 ) -> BatchFeature:
1564 - valid_mm_items = mm_items.select(
1565 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1567 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1568 + mm_items, hf_kwargs
1569 )
1567 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1570
1569 - if not mm_data:
1570 - return BatchFeature(dict(passthrough_data))
1571 + if not hf_data:
1572 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1573
1572 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
1574 + prompt_text = hf_data.pop("text")
1575 + assert isinstance(prompt_text, str)
1576
1574 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1575 - merged_kwargs = self.info.ctx.get_merged_mm_kwargs(hf_processor_mm_kwargs)
1577 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
1578 + merged_kwargs = self.info.ctx.get_merged_mm_kwargs(hf_kwargs)
1579 merged_kwargs.setdefault("return_tensors", "pt")
1580 call_kwargs = {
1581 k: v
@@ -1589,17 +1592,17 @@ class LlavaOnevision2MultiModalProcessor(
1592 "codec_config",
1593 }
1594 }
1592 - mm_data = dict(mm_data)
1595 +
1596 # Explicit None + length checks: ``mm_data[...]`` may be a list, numpy
1597 # array, or tensor, and ``and <array>`` would raise on the ambiguous
1598 # truth value of a multi-element array.
1596 - _videos = mm_data.get("videos")
1599 + _videos = hf_data.get("videos")
1600 if _videos is not None and not isinstance(_videos, Sized):
1601 raise TypeError(f"object of type '{type(_videos).__name__}' has no len()")
1602 videos_present = _videos is not None and len(_videos) > 0
1603
1604 codec_video_paths = (
1602 - _extract_codec_video_paths(mm_data["videos"]) if videos_present else None
1605 + _extract_codec_video_paths(hf_data["videos"]) if videos_present else None
1606 )
1607 is_codec_marker = codec_video_paths is not None
1608 # Fallback: caller passed video_backend=codec via mm_processor_kwargs
@@ -1613,7 +1616,7 @@ class LlavaOnevision2MultiModalProcessor(
1616 and call_kwargs.get("video_backend") == "codec"
1617 )
1618 if is_codec_kwarg:
1616 - raw = mm_data["videos"]
1619 + raw = hf_data["videos"]
1620 if isinstance(raw, str):
1621 codec_video_paths = [raw]
1622 elif isinstance(raw, (list, tuple)) and all(
@@ -1638,7 +1641,7 @@ class LlavaOnevision2MultiModalProcessor(
1641 # Codec backend: HF processor consumes the path string directly and
1642 # performs decode + canvas-packing internally. The dummy ndarray
1643 # we attached during prepare_codec_video_input is discarded here.
1641 - mm_data["videos"] = (
1644 + hf_data["videos"] = (
1645 codec_video_paths
1646 if len(codec_video_paths) > 1
1647 else codec_video_paths[0]
@@ -1648,11 +1651,9 @@ class LlavaOnevision2MultiModalProcessor(
1651 # ``video_backend="codec"`` via mm_kwargs so the wrapped processor
1652 # dispatches to its codec branch.
1653 output = self.info.ctx.call_hf_processor(
1651 - self.info.get_hf_processor(
1652 - **{**hf_processor_mm_kwargs, "video_backend": "codec"}
1653 - ),
1654 - dict(text=prompt_text, **mm_data),
1655 - {**hf_processor_mm_kwargs, "video_backend": "codec"},
1654 + self.info.get_hf_processor(**{**hf_kwargs, "video_backend": "codec"}),
1655 + dict(text=prompt_text, **hf_data),
1656 + {**hf_kwargs, "video_backend": "codec"},
1657 )
1658 data = dict(output)
1659 processed_data = BatchFeature(
@@ -1660,8 +1661,9 @@ class LlavaOnevision2MultiModalProcessor(
1661 data, codec_video_paths, hf_processor
1662 )
1663 )
1663 - processed_data.update(passthrough_data)
1664 - return processed_data
1664 + return self._finalize_hf_mm_data(
1665 + hf_data, hf_kwargs, passthrough_data, processed_data
1666 + )
1667
1668 # ---- Frame backend (registered LlavaOnevision2VideoBackend) ------
1669 # Every non-codec video reaches here as a ``(frames_ndarray, metadata)``
@@ -1676,7 +1678,7 @@ class LlavaOnevision2MultiModalProcessor(
1678 # provided by the backend's ``compute_frames_index_to_sample``; SSRF /
1679 # local-file gating is enforced by the connector before decoding.
1680 if videos_present:
1679 - timestamp_decimals = hf_processor_mm_kwargs.get(
1681 + timestamp_decimals = hf_kwargs.get(
1682 "timestamp_decimals", _DEFAULT_TIMESTAMP_DECIMALS
1683 )
1684 if not isinstance(
@@ -1690,7 +1692,7 @@ class LlavaOnevision2MultiModalProcessor(
1692
1693 per_video_frames: list[list[Image.Image]] = []
1694 per_video_timestamps: list[list[float]] = []
1693 - videos = mm_data["videos"]
1695 + videos = hf_data["videos"]
1696 if not isinstance(videos, Iterable):
1697 raise TypeError(f"Unsupported videos input: {type(videos)}")
1698 for item in videos:
@@ -1714,7 +1716,7 @@ class LlavaOnevision2MultiModalProcessor(
1716 # order) -- otherwise mixed image+video requests bind frames to the
1717 # wrong placeholder. ``row_is_video`` labels each grid row so outputs
1718 # can be split back into per-modality keys below.
1717 - merged_mm_data = dict(mm_data)
1719 + merged_mm_data = dict(hf_data)
1720 existing_images = merged_mm_data.pop("images", None)
1721 caller_images: list[Image.Image] = []
1722 if existing_images:
@@ -1760,9 +1762,9 @@ class LlavaOnevision2MultiModalProcessor(
1762 # **kwargs to the image processor, so passing the full merged kwarg
1763 # set here is a no-op beyond return_tensors/padding.
1764 output = self.info.ctx.call_hf_processor(
1763 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
1765 + self.info.get_hf_processor(**hf_kwargs),
1766 dict(text=new_prompt, **merged_mm_data),
1765 - hf_processor_mm_kwargs,
1767 + hf_kwargs,
1768 )
1769 data = dict(output)
1770
@@ -1820,20 +1822,22 @@ class LlavaOnevision2MultiModalProcessor(
1822 )
1823
1824 processed_data = BatchFeature(data)
1823 - processed_data.update(passthrough_data)
1824 - return processed_data
1825 + return self._finalize_hf_mm_data(
1826 + hf_data, hf_kwargs, passthrough_data, processed_data
1827 + )
1828
1829 # ---- Image-only / text-only call --------------------------------
1830 # No videos present: delegate to ``ctx.call_hf_processor``, which
1831 # runs the wrapped processor over the (possibly empty) image set and
1832 # applies float-tensor dtype postprocessing automatically.
1833 processed_data = self.info.ctx.call_hf_processor(
1831 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
1832 - dict(text=prompt_text, **mm_data),
1833 - hf_processor_mm_kwargs,
1834 + self.info.get_hf_processor(**hf_kwargs),
1835 + dict(text=prompt_text, **hf_data),
1836 + hf_kwargs,
1837 + )
1838 + return self._finalize_hf_mm_data(
1839 + hf_data, hf_kwargs, passthrough_data, processed_data
1840 )
1835 - processed_data.update(passthrough_data)
1836 - return processed_data
1841
1842 def _rename_codec_outputs_to_video(
1843 self,
vllm/model_executor/models/midashenglm.py
+24 -25
@@ -61,6 +61,7 @@ from vllm.multimodal.processing import (
61 PromptUpdate,
62 PromptUpdateDetails,
63 )
64 +from vllm.multimodal.processing.processor import HFMultiModalInputs
65 from vllm.sequence import IntermediateTensors
66 from vllm.transformers_utils.configs.midashenglm import DashengConfig
67 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -582,35 +583,33 @@ class MiDashengLMDummyInputsBuilder(BaseDummyInputsBuilder[MiDashengLMProcessing
583 class MiDashengLMMultiModalProcessor(
584 BaseMultiModalProcessor[MiDashengLMProcessingInfo]
585 ):
585 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
586 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
587 return self.dummy_inputs.get_dummy_text(mm_counts)
588
588 - def _preprocess_hf_mm_data(
589 + def _get_hf_mm_inputs(
590 self,
590 - mm_data: Mapping[str, object],
591 - hf_processor_mm_kwargs: Mapping[str, object],
592 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
593 - mm_data = dict(mm_data)
594 - audios = mm_data.pop("audios", [])
595 - assert isinstance(audios, list)
596 -
597 - min_audio_len = self.info.get_min_audio_len()
598 - processed_audios = [
599 - np.pad(
600 - audio,
601 - (0, min_audio_len - audio.shape[-1]),
602 - mode="constant",
603 - constant_values=0,
604 - )
605 - if isinstance(audio, np.ndarray) and audio.shape[-1] < min_audio_len
606 - else audio
607 - for audio in audios
608 - ]
609 -
610 - if processed_audios:
611 - mm_data["audio"] = processed_audios
591 + mm_items: MultiModalDataItems,
592 + hf_kwargs: Mapping[str, object],
593 + ) -> HFMultiModalInputs:
594 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
595 +
596 + if audios := hf_inputs.hf_data.get("audio"):
597 + assert isinstance(audios, list)
598 +
599 + min_audio_len = self.info.get_min_audio_len()
600 + hf_inputs.hf_data["audio"] = [
601 + np.pad(
602 + audio,
603 + (0, min_audio_len - audio.shape[-1]),
604 + mode="constant",
605 + constant_values=0,
606 + )
607 + if isinstance(audio, np.ndarray) and audio.shape[-1] < min_audio_len
608 + else audio
609 + for audio in audios
610 + ]
611
613 - return mm_data, hf_processor_mm_kwargs
612 + return hf_inputs
613
614 def _get_mm_fields_config(
615 self,
vllm/model_executor/models/mimo_v2_omni.py
+10 -28
@@ -46,6 +46,7 @@ from vllm.multimodal.processing import (
46 PromptUpdate,
47 PromptUpdateDetails,
48 )
49 +from vllm.multimodal.processing.processor import HFMultiModalInputs
50 from vllm.transformers_utils.configs.mimo_v2_omni import Mimo_VLVisionConfig
51 from vllm.transformers_utils.processors.mimo_v2_omni import (
52 MiMoOmniProcessor,
@@ -930,27 +931,14 @@ class MiMoV2OmniMultiModalProcessor(BaseMultiModalProcessor[MiMoV2OmniProcessing
931 fields["va_audio_features"] = MultiModalFieldConfig.batched("va_audio")
932 return fields
933
933 - def _apply_hf_processor_main(
934 + def _get_hf_mm_inputs(
935 self,
936 mm_items: MultiModalDataItems,
936 - hf_processor_mm_kwargs: Mapping[str, object],
937 - ) -> BatchFeature:
938 - """Convert numpy video arrays to (TCHW, timestamps) tuples for MiMo.
939 - Also remap 'audios' → 'audio' since MiMoOmniProcessor.__call__ uses
940 - the singular form.
941 - """
942 - valid_mm_items = mm_items.select(
943 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
944 - )
945 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
946 -
947 - if not mm_data:
948 - return BatchFeature(dict(passthrough_data))
949 -
950 - # Remap audios → audio (MiMoOmniProcessor uses singular param name)
951 - if "audios" in mm_data:
952 - mm_data = {**mm_data, "audio": mm_data["audios"]}
953 - mm_data = {k: v for k, v in mm_data.items() if k != "audios"}
937 + hf_kwargs: Mapping[str, object],
938 + ) -> HFMultiModalInputs:
939 + """Convert numpy video arrays to (TCHW, timestamps) tuples for MiMo."""
940 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
941 + mm_data = hf_inputs.hf_data
942
943 # Handle video_audio items: convert video part to (TCHW, timestamps) tuple
944 if "video_audio" in mm_data:
@@ -992,7 +980,7 @@ class MiMoV2OmniMultiModalProcessor(BaseMultiModalProcessor[MiMoV2OmniProcessing
980 audio=va_item.audio,
981 )
982 )
995 - mm_data = {**mm_data, "video_audio": va_converted}
983 + mm_data["video_audio"] = va_converted
984
985 if "videos" in mm_data:
986 converted: list[tuple[torch.Tensor, torch.Tensor]] = []
@@ -1026,15 +1014,9 @@ class MiMoV2OmniMultiModalProcessor(BaseMultiModalProcessor[MiMoV2OmniProcessing
1014 timestamps = torch.arange(T, dtype=torch.float32) / self._INPUT_FPS
1015 converted.append((frames, timestamps))
1016
1029 - mm_data = {**mm_data, "videos": converted}
1017 + mm_data["videos"] = converted
1018
1031 - processed_data = self.info.ctx.call_hf_processor(
1032 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
1033 - mm_data,
1034 - hf_processor_mm_kwargs,
1035 - )
1036 - processed_data.update(passthrough_data)
1037 - return processed_data
1019 + return hf_inputs
1020
1021 def _get_prompt_updates(
1022 self,
vllm/model_executor/models/minicpmo.py
+1 -1
@@ -445,7 +445,7 @@ class MiniCPMOMultiModalProcessor(MiniCPMVMultiModalProcessor[MiniCPMOProcessing
445 mm_data: Mapping[str, object],
446 mm_kwargs: Mapping[str, object],
447 ) -> Mapping[str, NestedTensors]:
448 - if (audios := mm_data.get("audios")) is None:
448 + if (audios := mm_data.get("audio")) is None:
449 return {}
450
451 mm_items = self.info.parse_mm_data({"audio": audios}, validate=False)
vllm/model_executor/models/minicpmv.py
+7 -7
@@ -1059,19 +1059,18 @@ class MiniCPMVMultiModalProcessor(BaseMultiModalProcessor[_I]):
1059 def _apply_hf_processor_main(
1060 self,
1061 mm_items: MultiModalDataItems,
1062 - hf_processor_mm_kwargs: Mapping[str, object],
1062 + hf_kwargs: Mapping[str, object],
1063 ) -> BatchFeature:
1064 - valid_mm_items = mm_items.select(
1065 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1064 + mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1065 + mm_items, hf_kwargs
1066 )
1067 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1067
1068 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
1069
1070 tokenizer = self.info.get_tokenizer()
1071
1072 input_ids = torch.tensor([tokenizer.encode(prompt_text)])
1074 - mm_inputs = self.process_mm_inputs(mm_data, hf_processor_mm_kwargs)
1073 + mm_inputs = self.process_mm_inputs(mm_data, hf_kwargs)
1074
1075 processed_data = BatchFeature(
1076 {
@@ -1079,8 +1078,9 @@ class MiniCPMVMultiModalProcessor(BaseMultiModalProcessor[_I]):
1078 **mm_inputs,
1079 }
1080 )
1082 - processed_data.update(passthrough_data)
1083 - return processed_data
1081 + return self._finalize_hf_mm_data(
1082 + mm_data, hf_kwargs, passthrough_data, processed_data
1083 + )
1084
1085 def _get_prompt_updates(
1086 self,
vllm/model_executor/models/mistral3.py
+2 -2
@@ -248,7 +248,7 @@ class Mistral3ProcessingInfo(BaseProcessingInfo):
248 return ImageSize(width=width, height=height)
249
250
251 -class Mistral3DummyInputsBuilder(BaseDummyInputsBuilder[Mistral3ProcessingInfo]):
251 +class Mistral3DummyInputsBuilder(BaseDummyInputsBuilder):
252 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
253 num_images = mm_counts.get("image", 0)
254
@@ -280,7 +280,7 @@ class Mistral3DummyInputsBuilder(BaseDummyInputsBuilder[Mistral3ProcessingInfo])
280
281
282 class Mistral3MultiModalProcessor(BaseMultiModalProcessor[Mistral3ProcessingInfo]):
283 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
283 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
284 return self.dummy_inputs.get_dummy_text(mm_counts)
285
286 def _postprocess_hf_mm_data(
vllm/model_executor/models/mllama4.py
+1 -1
@@ -589,7 +589,7 @@ class Mllama4ProcessingInfo(BaseProcessingInfo):
589
590
591 class Mllama4MultiModalProcessor(BaseMultiModalProcessor[Mllama4ProcessingInfo]):
592 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
592 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
593 return self.dummy_inputs.get_dummy_text(mm_counts)
594
595 def _postprocess_hf_mm_data(
vllm/model_executor/models/molmo.py
+21 -20
@@ -1122,6 +1122,9 @@ class MolmoDummyInputsBuilder(BaseDummyInputsBuilder[MolmoProcessingInfo]):
1122
1123
1124 class MolmoMultiModalProcessor(BaseMultiModalProcessor[MolmoProcessingInfo]):
1125 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1126 + return self.dummy_inputs.get_dummy_text(mm_counts)
1127 +
1128 def _postprocess_prompt(self, prompt: list[int]) -> list[int]:
1129 processor = self.info.get_hf_processor()
1130
@@ -1140,28 +1143,28 @@ class MolmoMultiModalProcessor(BaseMultiModalProcessor[MolmoProcessingInfo]):
1143 dict(tokens=tokens),
1144 )["input_ids"].tolist()
1145
1143 - def _apply_hf_processor_main(
1146 + def _call_hf_processor(
1147 self,
1145 - mm_items: MultiModalDataItems,
1146 - hf_processor_mm_kwargs: Mapping[str, object],
1148 + hf_data: Mapping[str, object],
1149 + hf_kwargs: Mapping[str, object],
1150 ) -> BatchFeature:
1148 - mm_counts = mm_items.get_all_counts()
1149 -
1150 - valid_mm_items = mm_items.select({k for k, c in mm_counts.items() if c > 0})
1151 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1152 -
1153 - if not processor_data:
1154 - return BatchFeature(dict(passthrough_data))
1155 -
1156 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1157 - prompt_text = self.dummy_inputs.get_dummy_text(mm_counts)
1158 -
1159 - processed_data = self.info.ctx.call_hf_processor(
1151 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
1152 + return self.info.ctx.call_hf_processor(
1153 hf_processor.process,
1161 - dict(text=prompt_text, **processor_data),
1162 - hf_processor_mm_kwargs,
1154 + hf_data,
1155 + hf_kwargs,
1156 )
1157
1158 + def _postprocess_hf_mm_data(
1159 + self,
1160 + hf_data: Mapping[str, object],
1161 + hf_kwargs: Mapping[str, object],
1162 + processed_data: BatchFeature,
1163 + ) -> BatchFeature:
1164 + if not hf_data:
1165 + return processed_data
1166 +
1167 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
1168 tokenizer = hf_processor.tokenizer
1169 image_patch_id = tokenizer.vocab[IMAGE_PATCH_TOKEN]
1170
@@ -1169,7 +1172,7 @@ class MolmoMultiModalProcessor(BaseMultiModalProcessor[MolmoProcessingInfo]):
1172
1173 processed_data.pop("input_ids")
1174
1172 - if (images := processor_data.get("images")) is not None:
1175 + if (images := hf_data.get("images")) is not None:
1176 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
1177 parsed_images = mm_items.get_items("image", ImageProcessorItems)
1178 image_sizes = [
@@ -1193,8 +1196,6 @@ class MolmoMultiModalProcessor(BaseMultiModalProcessor[MolmoProcessingInfo]):
1196 processed_data["num_crops"] = num_crops
1197 processed_data["img_patch_id"] = image_patch_id
1198
1196 - processed_data.update(passthrough_data)
1197 -
1199 return processed_data
1200
1201 def _get_mm_fields_config(
vllm/model_executor/models/molmo2.py
+23 -18
@@ -1932,6 +1932,9 @@ class Molmo2DummyInputsBuilder(BaseDummyInputsBuilder[Molmo2ProcessingInfo]):
1932
1933
1934 class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
1935 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1936 + return self.dummy_inputs.get_dummy_text(mm_counts)
1937 +
1938 def _postprocess_prompt(self, prompt: list[int]) -> list[int]:
1939 processor = self.info.get_hf_processor()
1940 tokenizer = processor.tokenizer
@@ -1946,19 +1949,20 @@ class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
1949 def _apply_hf_processor_main(
1950 self,
1951 mm_items: MultiModalDataItems,
1949 - hf_processor_mm_kwargs: Mapping[str, object],
1952 + hf_kwargs: Mapping[str, object],
1953 ) -> BatchFeature:
1951 - mm_counts = mm_items.get_all_counts()
1952 -
1953 - valid_mm_items = mm_items.select({k for k, c in mm_counts.items() if c > 0})
1954 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1954 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1955 + mm_items, hf_kwargs
1956 + )
1957
1956 - prompt_text = self.dummy_inputs.get_dummy_text(mm_counts)
1958 + if not hf_data:
1959 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1960
1958 - mm_data = dict(processor_data)
1961 + prompt_text = hf_data.pop("text")
1962 + assert isinstance(prompt_text, str)
1963
1964 hf_config = self.info.get_hf_config()
1961 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1965 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
1966
1967 def patched_call(text=None, images=None, videos=None, **kwargs) -> BatchFeature:
1968 res = hf_processor(text=text, images=images, videos=videos, **kwargs)
@@ -1973,7 +1977,7 @@ class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
1977 tokenizer = hf_processor.tokenizer
1978 image_processor = hf_processor.image_processor
1979
1976 - if videos := mm_data.pop("videos", []):
1980 + if videos := hf_data.pop("videos", []):
1981 assert isinstance(videos, Sequence)
1982 bos_token_id = tokenizer.bos_token_id or tokenizer.eos_token_id
1983
@@ -1993,16 +1997,16 @@ class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
1997 # NOTE: metadata.frames_indices indicates
1998 # the sampled frames indices of pre-sampled videos, which is
1999 # used to calculate the timestamps. Make sure that
1996 - # do_sample_frames in hf_processor_mm_kwargs is false for
2000 + # do_sample_frames in hf_kwargs is false for
2001 # presampled videos.
2002
1999 - # NOTE: a copy of hf_processor_mm_kwargs is created to update
2003 + # NOTE: a copy of hf_kwargs is created to update
2004 # do_sample_frames, otherwise mm_hash for the object will be
2005 # incorrect.
2002 - video_mm_kwargs = dict(**hf_processor_mm_kwargs)
2006 + video_mm_kwargs = dict(**hf_kwargs)
2007 if "do_sample_frames" not in video_mm_kwargs:
2008 # molmo_utils already has "do_sample_frames" in
2005 - # hf_processor_mm_kwargs, don't overwrite it.
2009 + # hf_kwargs, don't overwrite it.
2010 video_mm_kwargs["do_sample_frames"] = metadata.get(
2011 "do_sample_frames", False
2012 )
@@ -2067,11 +2071,11 @@ class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
2071
2072 processed_data = self.info.ctx.call_hf_processor(
2073 patched_call,
2070 - dict(text=prompt_text, **mm_data),
2071 - hf_processor_mm_kwargs,
2074 + dict(text=prompt_text, **hf_data),
2075 + hf_kwargs,
2076 )
2077
2074 - if (images := mm_data.get("images")) is not None:
2078 + if (images := hf_data.get("images")) is not None:
2079 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
2080 parsed_images = mm_items.get_items("image", ImageProcessorItems)
2081 image_sizes = [
@@ -2113,10 +2117,11 @@ class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
2117 )
2118
2119 processed_data.update(all_video_outputs)
2116 - processed_data.update(passthrough_data)
2120 processed_data.pop("input_ids")
2121
2119 - return processed_data
2122 + return self._finalize_hf_mm_data(
2123 + hf_data, hf_kwargs, passthrough_data, processed_data
2124 + )
2125
2126 def _get_mm_fields_config(
2127 self,
vllm/model_executor/models/moss_audio.py
+6 -21
@@ -1356,32 +1356,17 @@ class MossAudioDummyInputsBuilder(BaseDummyInputsBuilder[MossAudioProcessingInfo
1356
1357
1358 class MossAudioMultiModalProcessor(BaseMultiModalProcessor[MossAudioProcessingInfo]):
1359 - def _apply_hf_processor_main(
1359 + def _call_hf_processor(
1360 self,
1361 - mm_items: MultiModalDataItems,
1362 - hf_processor_mm_kwargs: Mapping[str, object],
1361 + hf_data: Mapping[str, object],
1362 + hf_kwargs: Mapping[str, object],
1363 ) -> BatchFeature:
1364 - valid_mm_items = mm_items.select(
1365 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1366 - )
1367 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1368 -
1369 - if not mm_data:
1370 - return BatchFeature(dict(passthrough_data))
1371 -
1372 - mm_data = dict(mm_data)
1373 - audios = mm_data.pop("audios", [])
1374 - if audios:
1375 - mm_data["audio"] = audios
1376 - hf_processor_mm_kwargs = dict(hf_processor_mm_kwargs)
1377 - processor_kwargs = _filter_moss_audio_processor_config(hf_processor_mm_kwargs)
1378 - processed_data = self.info.ctx.call_hf_processor(
1364 + processor_kwargs = _filter_moss_audio_processor_config(dict(hf_kwargs))
1365 + return self.info.ctx.call_hf_processor(
1366 self.info.get_hf_processor(**processor_kwargs),
1380 - mm_data,
1367 + hf_data,
1368 {},
1369 )
1383 - processed_data.update(passthrough_data)
1384 - return processed_data
1370
1371 def _get_mm_fields_config(
1372 self,
vllm/model_executor/models/moss_transcribe_diarize.py
+10 -8
@@ -66,7 +66,7 @@ from vllm.multimodal.processing import (
66 PromptUpdateDetails,
67 cached_encode,
68 )
69 -from vllm.multimodal.processing.processor import ProcessorInputs
69 +from vllm.multimodal.processing.processor import HFMultiModalInputs, ProcessorInputs
70 from vllm.sequence import IntermediateTensors
71 from vllm.transformers_utils.processor import cached_processor_from_config
72 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -465,17 +465,19 @@ class MossTranscribeDiarizeDummyInputsBuilder(
465 class MossTranscribeDiarizeMultiModalProcessor(
466 BaseMultiModalProcessor[MossTranscribeDiarizeProcessingInfo]
467 ):
468 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
468 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
469 return self.dummy_inputs.get_dummy_text(mm_counts)
470
471 - def _preprocess_hf_mm_data(
471 + def _get_hf_mm_inputs(
472 self,
473 - mm_data: Mapping[str, object],
474 - hf_processor_mm_kwargs: Mapping[str, object],
475 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
476 - audios = _get_audios_from_mm_data(mm_data)
473 + mm_items: MultiModalDataItems,
474 + hf_kwargs: Mapping[str, object],
475 + ) -> HFMultiModalInputs:
476 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
477 + if hf_inputs.hf_data:
478 + hf_inputs.hf_data["audio"] = _get_audios_from_mm_data(hf_inputs.hf_data)
479
478 - return dict(audio=audios), hf_processor_mm_kwargs
480 + return hf_inputs
481
482 def _postprocess_hf_mm_data(
483 self,
vllm/model_executor/models/muse_glimmer.py
+10 -7
@@ -275,16 +275,18 @@ class MuseGlimmerMultiModalProcessor(
275 def _apply_hf_processor_main(
276 self,
277 mm_items: MultiModalDataItems,
278 - hf_processor_mm_kwargs: Mapping[str, object],
278 + hf_kwargs: Mapping[str, object],
279 ) -> BatchFeature:
280 - valid_mm_items = mm_items.select(
281 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
280 + mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
281 + mm_items, hf_kwargs
282 )
283 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
283 +
284 + if not mm_data:
285 + return self._finalize_hf_mm_data(mm_data, hf_kwargs, passthrough_data)
286
287 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
288
287 - processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
289 + processor = self.info.get_hf_processor(**hf_kwargs)
290 tokenizer = processor.tokenizer
291 config = self.info.get_hf_config()
292 images = mm_data.get("images", ())
@@ -371,8 +373,9 @@ class MuseGlimmerMultiModalProcessor(
373 video_feature_sizes=torch.tensor(video_sizes),
374 )
375 processed_data = BatchFeature(data=data, tensor_type=None)
374 - processed_data.update(passthrough_data)
375 - return processed_data
376 + return self._finalize_hf_mm_data(
377 + mm_data, hf_kwargs, passthrough_data, processed_data
378 + )
379
380 def _get_mm_fields_config(
381 self,
vllm/model_executor/models/nano_nemotron_vl.py
+23 -8
@@ -70,6 +70,7 @@ from vllm.multimodal.processing import (
70 from vllm.multimodal.processing.processor import (
71 BaseMultiModalProcessor,
72 BaseProcessingInfo,
73 + HFMultiModalInputs,
74 PromptReplacement,
75 PromptUpdate,
76 cached_encode,
@@ -196,6 +197,9 @@ NanoNemotronVLVideoInputs: TypeAlias = (
197
198 class NanoNemotronVLProcessingInfo(BaseProcessingInfo):
199 def get_hf_processor(self, **kwargs: object) -> NanoNemotronVLProcessor:
200 + # Not accepted by NanoNemotronVLProcessor.__init__
201 + kwargs.pop("truncation", None)
202 +
203 return self.ctx.init_processor(
204 NanoNemotronVLProcessor,
205 config=self.get_hf_config(),
@@ -358,9 +362,23 @@ class NanoNemotronVLProcessingInfo(BaseProcessingInfo):
362 class NanoNemotronVLMultiModalProcessor(
363 BaseMultiModalProcessor[NanoNemotronVLProcessingInfo]
364 ):
361 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
365 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
366 return self.dummy_inputs.get_dummy_text(mm_counts)
367
368 + def _get_hf_mm_inputs(
369 + self,
370 + mm_items: MultiModalDataItems,
371 + hf_kwargs: Mapping[str, object],
372 + ) -> HFMultiModalInputs:
373 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
374 +
375 + # NanoNemotronVLProcessor expects "audios" instead of "audio"
376 + hf_data = hf_inputs.hf_data
377 + if "audio" in hf_data:
378 + hf_data["audios"] = hf_data.pop("audio")
379 +
380 + return hf_inputs
381 +
382 def _get_image_fields_config(self, hf_inputs: BatchFeature):
383 if self.info.is_dynamic_tiler:
384 pixel_values_flat = MultiModalFieldConfig.batched("image")
@@ -730,15 +748,12 @@ class NanoNemotronVLMultiModalProcessor(
748 # Bypass the cached path: the HF processor must receive the
749 # prompt (with injected <so_embedding>) and the audio data
750 # together so it can perform audio-token replacement natively.
733 - mm_info = self._apply_hf_processor(inputs, timing_ctx)
734 - prompt_ids = self._postprocess_prompt(inputs.prompt)
751 + mm_res = self._apply_hf_processor(inputs, timing_ctx)
752
753 with timing_ctx.record("apply_prompt_updates"):
754 prompt_ids, mm_placeholders = self._maybe_apply_prompt_updates(
755 mm_items=mm_items,
739 - prompt_ids=prompt_ids,
740 - mm_kwargs=mm_info.kwargs,
741 - mm_prompt_updates=mm_info.prompt_updates,
756 + mm_res=mm_res,
757 )
758
759 mm_placeholder_ranges = {
@@ -749,8 +764,8 @@ class NanoNemotronVLMultiModalProcessor(
764 return MultiModalInput(
765 type="multimodal",
766 prompt_token_ids=prompt_ids,
752 - mm_kwargs=mm_info.kwargs,
753 - mm_hashes=mm_info.hashes,
767 + mm_kwargs=mm_res.kwargs,
768 + mm_hashes=mm_res.hashes,
769 mm_placeholders=mm_placeholder_ranges,
770 )
771
vllm/model_executor/models/ovis.py
+3 -3
@@ -326,6 +326,9 @@ class OvisDummyInputsBuilder(BaseDummyInputsBuilder[OvisProcessingInfo]):
326
327
328 class OvisMultiModalProcessor(BaseMultiModalProcessor[OvisProcessingInfo]):
329 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
330 + return self.dummy_inputs.get_dummy_text(mm_counts)
331 +
332 def image_indicators_to_visual_tokens(
333 self,
334 image_indicators: list[int],
@@ -340,9 +343,6 @@ class OvisMultiModalProcessor(BaseMultiModalProcessor[OvisProcessingInfo]):
343 # -300 is image_atom token, filter them out
344 return [vte_vocab_size + x + 300 for x in image_indicators if x < -300]
345
343 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
344 - return self.dummy_inputs.get_dummy_text(mm_counts)
345 -
346 def _postprocess_hf_mm_data(
347 self,
348 mm_data: Mapping[str, object],
vllm/model_executor/models/ovis2_5.py
+1 -1
@@ -327,7 +327,7 @@ class Ovis2_5MultiModalProcessor(BaseMultiModalProcessor[Ovis2_5ProcessingInfo])
327 if x >= INDICATOR_IDS[0]
328 ]
329
330 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
330 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
331 return self.dummy_inputs.get_dummy_text(mm_counts)
332
333 def _postprocess_hf_mm_data(
vllm/model_executor/models/paddleocr_vl.py
+20 -19
@@ -243,36 +243,37 @@ class PaddleOCRVLDummyInputsBuilder(BaseDummyInputsBuilder[PaddleOCRVLProcessing
243 class PaddleOCRVLMultiModalProcessor(
244 BaseMultiModalProcessor[PaddleOCRVLProcessingInfo]
245 ):
246 - def _apply_hf_processor_main(
246 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
247 + return self.dummy_inputs.get_dummy_text(mm_counts)
248 +
249 + def _call_hf_processor(
250 self,
248 - mm_items: MultiModalDataItems,
249 - hf_processor_mm_kwargs: Mapping[str, object],
251 + hf_data: Mapping[str, object],
252 + hf_kwargs: Mapping[str, object],
253 ) -> BatchFeature:
251 - valid_mm_items = mm_items.select(
252 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
253 - )
254 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
255 -
256 - if not mm_data:
257 - return BatchFeature(dict(passthrough_data))
258 -
259 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
260 -
261 - final_mm_kwargs = dict(hf_processor_mm_kwargs or {})
254 + final_mm_kwargs = dict(hf_kwargs or {})
255 final_mm_kwargs.setdefault("images_kwargs", {})
256 # vLLM use PIL.Image, always set channel_last
257 final_mm_kwargs["input_data_format"] = ChannelDimension.LAST
265 - processed_data = self.info.ctx.call_hf_processor(
258 + return self.info.ctx.call_hf_processor(
259 self.info.get_hf_processor(**final_mm_kwargs),
267 - dict(text=prompt_text, **mm_data),
268 - hf_processor_mm_kwargs,
260 + hf_data,
261 + hf_kwargs,
262 )
263 +
264 + def _postprocess_hf_mm_data(
265 + self,
266 + hf_data: Mapping[str, object],
267 + hf_kwargs: Mapping[str, object],
268 + processed_data: BatchFeature,
269 + ) -> BatchFeature:
270 + if not hf_data:
271 + return processed_data
272 +
273 num_patches_per_image = processed_data["image_grid_thw"].prod(-1)
274 processed_data["pixel_values"] = processed_data["pixel_values"].split(
275 num_patches_per_image.tolist()
276 )
274 - processed_data.update(passthrough_data)
275 -
277 return processed_data
278
279 def _get_mm_fields_config(
vllm/model_executor/models/phi3v.py
+1 -20
@@ -390,28 +390,9 @@ class Phi3VDummyInputsBuilder(BaseDummyInputsBuilder[Phi3VProcessingInfo]):
390
391
392 class Phi3VMultiModalProcessor(BaseMultiModalProcessor[Phi3VProcessingInfo]):
393 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
393 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
394 return self.dummy_inputs.get_dummy_text(mm_counts)
395
396 - def _postprocess_hf_mm_data(
397 - self,
398 - mm_data: Mapping[str, object],
399 - hf_processor_mm_kwargs: Mapping[str, object],
400 - processed_data: BatchFeature,
401 - ) -> BatchFeature:
402 - if not mm_data:
403 - return processed_data
404 -
405 - input_ids = processed_data["input_ids"]
406 - assert isinstance(input_ids, torch.Tensor)
407 -
408 - # Phi3v processor has inserted -1, -2 etc as placeholder in prompt_ids,
409 - # which will cause OverflowError when decoding the prompt_ids.
410 - # Therefore, we need to do an early replacement here
411 - input_ids.masked_fill_(input_ids < 0, _IMAGE_TOKEN_ID)
412 -
413 - return processed_data
414 -
396 def _get_mm_fields_config(
397 self,
398 hf_inputs: BatchFeature,
vllm/model_executor/models/phi4mm.py
+35 -31
@@ -45,6 +45,7 @@ from vllm.multimodal.processing import BaseDummyInputsBuilder
45 from vllm.multimodal.processing.processor import (
46 BaseMultiModalProcessor,
47 BaseProcessingInfo,
48 + HFMultiModalInputs,
49 PromptReplacement,
50 PromptUpdate,
51 ResolvedPromptUpdate,
@@ -867,40 +868,38 @@ class Phi4MMDummyInputsBuilder(BaseDummyInputsBuilder[Phi4MMProcessingInfo]):
868
869
870 class Phi4MMMultiModalProcessor(BaseMultiModalProcessor[Phi4MMProcessingInfo]):
870 - def _apply_hf_processor_main(
871 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
872 + return self.dummy_inputs.get_dummy_text(mm_counts)
873 +
874 + def _get_hf_mm_inputs(
875 self,
876 mm_items: MultiModalDataItems,
873 - hf_processor_mm_kwargs: Mapping[str, object],
874 - ) -> BatchFeature:
875 - valid_mm_items = mm_items.select(
876 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
877 - )
878 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
877 + hf_kwargs: Mapping[str, object],
878 + ) -> HFMultiModalInputs:
879 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
880
880 - if not mm_data:
881 - return BatchFeature(dict(passthrough_data))
881 + # The Phi4MM processor expects "audios" instead of "audio"
882 + hf_data = hf_inputs.hf_data
883 + if "audio" in hf_data:
884 + hf_data["audios"] = hf_data.pop("audio")
885
883 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
886 + sr = self.info.get_feature_extractor(**hf_inputs.hf_kwargs).sampling_rate
887 + if audio_data := hf_data.get("audios", []):
888 + assert isinstance(audio_data, Sequence)
889 + hf_data["audios"] = [(data, sr) for data in audio_data]
890
885 - sr = self.info.get_feature_extractor(**hf_processor_mm_kwargs).sampling_rate
886 - raw_audio_data = mm_data.get("audios", [])
887 - assert isinstance(raw_audio_data, Sequence)
888 - audio_data = []
889 - for data in raw_audio_data:
890 - assert isinstance(data, (list, np.ndarray, torch.Tensor))
891 - audio_data.append(data)
891 + return hf_inputs
892
893 - processor_data = dict(text=prompt_text, **mm_data)
894 - if audio_data:
895 - processor_data["audios"] = [(data, sr) for data in audio_data]
896 -
897 - processed_data = self.info.ctx.call_hf_processor(
898 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
899 - processor_data,
900 - hf_processor_mm_kwargs,
901 - )
893 + def _postprocess_hf_mm_data(
894 + self,
895 + hf_data: Mapping[str, object],
896 + hf_kwargs: Mapping[str, object],
897 + processed_data: BatchFeature,
898 + ) -> BatchFeature:
899 + if not hf_data:
900 + return processed_data
901
903 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
902 + hf_processor = self.info.get_hf_processor(**hf_kwargs)
903 num_img_tokens = [
904 self.info.get_num_image_tokens(
905 image_width=img_size[0],
@@ -911,21 +910,26 @@ class Phi4MMMultiModalProcessor(BaseMultiModalProcessor[Phi4MMProcessingInfo]):
910 ]
911 processed_data["num_img_tokens"] = num_img_tokens
912
914 - audio_features = processed_data["input_audio_embeds"]
913 + audios = hf_data.get("audios", [])
914 + assert isinstance(audios, list)
915 + audio_features = processed_data.get("input_audio_embeds")
916 + if audio_features is None:
917 + return processed_data
918 +
919 + sr = self.info.get_feature_extractor(**hf_kwargs).sampling_rate
920 feature_sizes = [
916 - self.info.get_audio_num_frames(len(audio), sr) for audio in audio_data
921 + self.info.get_audio_num_frames(len(audio), sr) for audio, _ in audios
922 ]
923 processed_data["input_audio_embeds"] = [
924 audio_features[idx, :size] for idx, size in enumerate(feature_sizes)
925 ]
921 - processed_data.update(passthrough_data)
926
927 return processed_data
928
929 def _get_mm_fields_config(
930 self,
931 hf_inputs: BatchFeature,
928 - hf_processor_mm_kwargs: Mapping[str, object],
932 + hf_kwargs: Mapping[str, object],
933 ) -> Mapping[str, MultiModalFieldConfig]:
934 return dict(
935 input_image_embeds=MultiModalFieldConfig.batched("image"),
vllm/model_executor/models/phi4siglip.py
+2 -33
@@ -143,39 +143,8 @@ class Phi4SiglipDummyInputsBuilder(
143 class Phi4SiglipMultiModalProcessor(
144 BaseMultiModalProcessor[Phi4SiglipProcessingInfo],
145 ):
146 - def _apply_hf_processor_main(
147 - self,
148 - mm_items: MultiModalDataItems,
149 - hf_processor_mm_kwargs: Mapping[str, object],
150 - ) -> BatchFeature:
151 - valid_mm_items = mm_items.select(
152 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
153 - )
154 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
155 -
156 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
157 -
158 - processed = self.info.ctx.call_hf_processor(
159 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
160 - dict(text=prompt_text, **mm_data),
161 - hf_processor_mm_kwargs,
162 - )
163 -
164 - # The HF processor's tokenizer_image_token() replaces the "<image>"
165 - # string with IMAGE_TOKEN_INDEX (-200) in input_ids. This breaks
166 - # vLLM's prompt-replacement pipeline which needs to find "<image>"
167 - # as normal sub-tokens. Re-tokenize with the plain tokenizer so
168 - # that "<image>" stays as sub-tokens and can be located by
169 - # PromptReplacement.
170 - # NOTE: tokenizer.__call__() (not .encode()) must be used so that
171 - # added/special tokens like <|user|>, <|end|> are kept as single IDs.
172 - tokenizer = self.info.get_tokenizer()
173 - new_ids = tokenizer(prompt_text).input_ids
174 - processed["input_ids"] = torch.tensor([new_ids])
175 -
176 - processed_data = processed
177 - processed_data.update(passthrough_data)
178 - return processed_data
146 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
147 + return self.dummy_inputs.get_dummy_text(mm_counts)
148
149 def _get_mm_fields_config(
150 self,
vllm/model_executor/models/pixtral.py
+16 -37
@@ -14,7 +14,7 @@ from mistral_common.protocol.instruct.chunk import ImageChunk, TextChunk
14 from mistral_common.protocol.instruct.messages import UserMessage
15 from mistral_common.protocol.instruct.request import ChatCompletionRequest
16 from packaging.version import Version
17 -from transformers import BatchFeature, PixtralVisionConfig
17 +from transformers import PixtralVisionConfig
18 from transformers.models.pixtral.image_processing_pixtral import (
19 _num_image_tokens as _get_pixtral_hf_num_image_tokens,
20 )
@@ -41,7 +41,6 @@ from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalKwargsItems
41 from vllm.multimodal.inputs import (
42 MultiModalFieldConfig,
43 MultiModalKwargsItem,
44 - MultiModalKwargsOptionalItems,
44 NestedTensors,
45 )
46 from vllm.multimodal.parse import (
@@ -53,7 +52,8 @@ from vllm.multimodal.processing import BaseDummyInputsBuilder
52 from vllm.multimodal.processing.processor import (
53 BaseMultiModalProcessor,
54 BaseProcessingInfo,
56 - MultiModalPromptUpdates,
55 + HFMultiModalInputs,
56 + MultiModalProcessingResult,
57 PlaceholderFeaturesInfo,
58 ProcessorInputs,
59 PromptReplacement,
@@ -67,7 +67,6 @@ from vllm.transformers_utils.processors.pixtral import (
67 MistralCommonImageProcessor,
68 MistralCommonPixtralProcessor,
69 )
70 -from vllm.utils.collection_utils import is_list_of
70 from vllm.utils.tensor_schema import TensorSchema, TensorShape
71 from vllm.v1.attention.backends.registry import AttentionBackendEnum
72
@@ -268,21 +267,19 @@ class PixtralMultiModalProcessor(BaseMultiModalProcessor[PixtralProcessingInfo])
267 def _maybe_apply_prompt_updates(
268 self,
269 mm_items: MultiModalDataItems,
271 - prompt_ids: list[int],
272 - mm_kwargs: MultiModalKwargsOptionalItems,
273 - mm_prompt_updates: MultiModalPromptUpdates,
270 + mm_res: MultiModalProcessingResult,
271 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
272 mm_item_counts = mm_items.get_all_counts()
276 - self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
277 - self._validate_mm_updates(mm_prompt_updates, mm_item_counts)
273 + self._validate_mm_kwargs(mm_res.kwargs, mm_item_counts)
274 + self._validate_mm_updates(mm_res.prompt_updates, mm_item_counts)
275
276 mm_placeholders = self._find_mm_placeholders(
280 - prompt_ids,
281 - mm_prompt_updates,
277 + mm_res.prompt_ids,
278 + mm_res.prompt_updates,
279 )
280 self._validate_mm_placeholders(mm_placeholders, mm_item_counts)
281
285 - return prompt_ids, mm_placeholders
282 + return mm_res.prompt_ids, mm_placeholders
283
284 def _get_mm_fields_config(
285 self,
@@ -291,36 +288,18 @@ class PixtralMultiModalProcessor(BaseMultiModalProcessor[PixtralProcessingInfo])
288 ) -> Mapping[str, MultiModalFieldConfig]:
289 return dict(images=MultiModalFieldConfig.batched("image"))
290
294 - def _apply_hf_processor_main(
291 + def _get_hf_mm_inputs(
292 self,
293 mm_items: MultiModalDataItems,
297 - hf_processor_mm_kwargs: Mapping[str, object],
298 - ) -> BatchFeature:
299 - valid_mm_items = mm_items.select(
300 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
301 - )
302 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
303 -
304 - if not mm_data:
305 - return BatchFeature(dict(passthrough_data))
294 + hf_kwargs: Mapping[str, object],
295 + ) -> HFMultiModalInputs:
296 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
297
307 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
308 -
309 - outputs = self.info.ctx.call_hf_processor(
310 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
311 - dict(text=prompt_text, **mm_data),
312 - # Avoid padding issue
313 - dict(**hf_processor_mm_kwargs, return_tensors=None),
298 + # Avoid padding issue
299 + return hf_inputs._replace(
300 + hf_kwargs=dict(hf_inputs.hf_kwargs, return_tensors=None)
301 )
302
316 - # Missing batch dimension
317 - if is_list_of(outputs["input_ids"], int):
318 - outputs["input_ids"] = [outputs["input_ids"]]
319 -
320 - processed_data = outputs
321 - processed_data.update(passthrough_data)
322 - return processed_data
323 -
303 def _get_prompt_updates(
304 self,
305 mm_items: MultiModalDataItems,
vllm/model_executor/models/qwen2_5_omni_thinker.py
+80 -86
@@ -82,6 +82,8 @@ from vllm.multimodal.processing import (
82 )
83 from vllm.multimodal.processing.processor import (
84 BaseMultiModalProcessor,
85 + HFMultiModalInputs,
86 + MultiModalProcessingResult,
87 MultiModalPromptUpdates,
88 PlaceholderFeaturesInfo,
89 PromptReplacement,
@@ -498,6 +500,64 @@ class Qwen2_5OmniThinkerDummyInputsBuilder(
500 class Qwen2_5OmniThinkerMultiModalProcessor(
501 BaseMultiModalProcessor[Qwen2_5OmniThinkerProcessingInfo]
502 ):
503 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
504 + return self.dummy_inputs.get_dummy_text(mm_counts)
505 +
506 + def _get_hf_mm_inputs(
507 + self,
508 + mm_items: MultiModalDataItems,
509 + hf_kwargs: Mapping[str, object],
510 + ) -> HFMultiModalInputs:
511 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
512 +
513 + mm_counts = mm_items.get_all_counts()
514 + if hf_kwargs.get("use_audio_in_video"):
515 + num_videos = mm_counts.get("video", 0)
516 + if mm_counts.get("audio", 0) < num_videos:
517 + raise ValueError(
518 + "Video doesn't have audio track with `audio_in_video=True`"
519 + )
520 +
521 + if num_videos and "text" in hf_inputs.hf_data:
522 + standalone_mm_counts = dict(mm_counts)
523 + standalone_mm_counts["audio"] -= num_videos
524 + hf_inputs.hf_data["text"] = self._get_hf_mm_text(standalone_mm_counts)
525 +
526 + hf_data = hf_inputs.hf_data
527 + normalized_kwargs = hf_inputs.hf_kwargs
528 + audios = hf_data.pop("audio", [])
529 +
530 + # NOTE: WhisperFeatureExtractor cannot handle empty list of audios
531 + if audios:
532 + hf_data["audio"] = audios
533 + elif (
534 + normalized_kwargs.get("use_audio_in_video")
535 + and "audio" not in hf_data
536 + and hf_data.get("videos")
537 + ):
538 + raise ValueError(
539 + "Video doesn't have audio track with `audio_in_video=True`"
540 + )
541 +
542 + merged = self.info.ctx.get_merged_mm_kwargs(normalized_kwargs)
543 + if hf_data.get("videos") and (
544 + merged.keys() & {"size", "min_pixels", "max_pixels"}
545 + ):
546 + normalized_kwargs = dict(normalized_kwargs)
547 + video_size = dict(self.info.get_hf_processor().video_processor.size)
548 + size_override = merged.get("size")
549 + if size_override is not None:
550 + video_size = video_size | size_override
551 + min_pixels = merged.get("min_pixels")
552 + if min_pixels is not None:
553 + video_size["shortest_edge"] = min_pixels
554 + max_pixels = merged.get("max_pixels")
555 + if max_pixels is not None:
556 + video_size["longest_edge"] = max_pixels
557 + normalized_kwargs["size"] = video_size
558 +
559 + return hf_inputs._replace(hf_kwargs=normalized_kwargs)
560 +
561 def _get_mm_fields_config(
562 self,
563 hf_inputs: BatchFeature,
@@ -570,12 +630,14 @@ class Qwen2_5OmniThinkerMultiModalProcessor(
630 def _maybe_apply_prompt_updates(
631 self,
632 mm_items: MultiModalDataItems,
573 - prompt_ids: list[int],
574 - mm_kwargs: MultiModalKwargsItems,
575 - mm_prompt_updates: MultiModalPromptUpdates,
633 + mm_res: MultiModalProcessingResult,
634 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
635 """Qwen2.5-Omni reimplements this function to handle `use_audio_in_video`."""
636 mm_item_counts = mm_items.get_all_counts()
637 + prompt_ids = mm_res.prompt_ids
638 + mm_kwargs = mm_res.kwargs
639 + mm_prompt_updates = mm_res.prompt_updates
640 +
641 self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
642 self._validate_mm_updates(mm_prompt_updates, mm_item_counts)
643
@@ -789,101 +851,33 @@ class Qwen2_5OmniThinkerMultiModalProcessor(
851 ),
852 ]
853
792 - def _apply_hf_processor_main(
854 + def _postprocess_hf_mm_data(
855 self,
794 - mm_items: MultiModalDataItems,
795 - hf_processor_mm_kwargs: Mapping[str, object],
856 + hf_data: Mapping[str, object],
857 + hf_kwargs: Mapping[str, object],
858 + processed_data: BatchFeature,
859 ) -> BatchFeature:
797 - """Qwen2.5-Omni reimplements this function to handle `use_audio_in_video`."""
798 - mm_counts = mm_items.get_all_counts()
799 -
800 - use_audio_in_video = hf_processor_mm_kwargs.get("use_audio_in_video", False)
801 - if use_audio_in_video and "video" in mm_counts:
802 - if mm_counts.get("audio", 0) < mm_counts["video"]:
803 - raise ValueError(
804 - "Video doesn't have audio track with `audio_in_video=True`"
805 - )
806 - mm_counts["audio"] -= mm_counts["video"]
807 -
808 - valid_mm_items = mm_items.select(
809 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
810 - )
811 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
812 -
813 - prompt_text = self.dummy_inputs.get_dummy_text(mm_counts)
814 -
815 - mm_data = dict(processor_data)
816 - audios = mm_data.pop("audios", [])
817 -
818 - # NOTE: WhisperFeatureExtractor cannot handle empty list of audios
819 - if audios:
820 - # NOTE: Qwen2.5-Omni processor accept "audio"
821 - mm_data["audio"] = audios
822 - hf_processor_mm_kwargs = dict(
823 - **hf_processor_mm_kwargs,
824 - )
825 - elif (
826 - hf_processor_mm_kwargs.get("use_audio_in_video")
827 - and "audio" not in mm_data
828 - and mm_data.get("videos")
829 - ):
830 - # A subclass (e.g. Qwen3-Omni) may have already popped "audios"
831 - # and populated mm_data["audio"] itself before delegating here,
832 - # in which case this isn't a genuine "no audio" case. Likewise,
833 - # mm_data can be empty on a multimodal-processor-cache hit, where
834 - # there's nothing to (re-)process this call and the real result
835 - # comes from the cache — not a genuine "no audio" case either.
836 - raise ValueError(
837 - "Video doesn't have audio track with `audio_in_video=True`"
838 - )
839 -
840 - merged = self.info.ctx.get_merged_mm_kwargs(hf_processor_mm_kwargs)
841 - if mm_data.get("videos") and (
842 - merged.keys() & {"size", "min_pixels", "max_pixels"}
843 - ):
844 - hf_processor_mm_kwargs = dict(hf_processor_mm_kwargs)
845 - video_size = dict(self.info.get_hf_processor().video_processor.size)
846 - size_override = merged.get("size")
847 - if size_override is not None:
848 - video_size = video_size | size_override
849 - min_pixels = merged.get("min_pixels")
850 - if min_pixels is not None:
851 - video_size["shortest_edge"] = min_pixels
852 - max_pixels = merged.get("max_pixels")
853 - if max_pixels is not None:
854 - video_size["longest_edge"] = max_pixels
855 - hf_processor_mm_kwargs["size"] = video_size
856 -
857 - hf_inputs = self.info.ctx.call_hf_processor(
858 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
859 - dict(text=prompt_text, **mm_data),
860 - hf_processor_mm_kwargs,
861 - )
862 -
863 - input_features = hf_inputs.pop("input_features", None)
864 - feature_attention_mask = hf_inputs.get("feature_attention_mask", None)
865 - if "input_audio_features" not in hf_inputs and input_features is not None:
860 + input_features = processed_data.pop("input_features", None)
861 + feature_attention_mask = processed_data.get("feature_attention_mask", None)
862 + if "input_audio_features" not in processed_data and input_features is not None:
863 if feature_attention_mask is not None:
864 input_features = input_features.permute(0, 2, 1)[
865 feature_attention_mask.bool()
866 ].permute(1, 0)
870 - hf_inputs["input_audio_features"] = input_features
867 + processed_data["input_audio_features"] = input_features
868 if (
872 - "audio_feature_lengths" not in hf_inputs
869 + "audio_feature_lengths" not in processed_data
870 and feature_attention_mask is not None
871 ):
875 - hf_inputs["audio_feature_lengths"] = feature_attention_mask.sum(-1)
872 + processed_data["audio_feature_lengths"] = feature_attention_mask.sum(-1)
873
877 - video_second_per_grid = hf_inputs.get("video_second_per_grid", None)
874 + video_second_per_grid = processed_data.get("video_second_per_grid", None)
875 if video_second_per_grid is not None:
879 - hf_inputs["second_per_grid_ts"] = video_second_per_grid
880 -
881 - use_audio_in_video = hf_processor_mm_kwargs.get("use_audio_in_video", False)
882 - hf_inputs["use_audio_in_video"] = torch.tensor(use_audio_in_video)
876 + processed_data["second_per_grid_ts"] = video_second_per_grid
877
884 - processed_data = hf_inputs
885 - processed_data.update(passthrough_data)
886 - processed_data.pop("input_ids")
878 + use_audio_in_video = hf_kwargs.get("use_audio_in_video", False)
879 + processed_data["use_audio_in_video"] = torch.tensor(use_audio_in_video)
880 + processed_data.pop("input_ids", None)
881
882 return processed_data
883
vllm/model_executor/models/qwen2_audio.py
+15 -18
@@ -59,6 +59,7 @@ from vllm.multimodal.processing import (
59 PromptReplacement,
60 PromptUpdate,
61 )
62 +from vllm.multimodal.processing.processor import HFMultiModalInputs
63 from vllm.sequence import IntermediateTensors
64 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
65 from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -233,29 +234,25 @@ class Qwen2AudioDummyInputsBuilder(BaseDummyInputsBuilder[Qwen2AudioProcessingIn
234
235
236 class Qwen2AudioMultiModalProcessor(BaseMultiModalProcessor[Qwen2AudioProcessingInfo]):
236 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
237 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
238 return self.dummy_inputs.get_dummy_text(mm_counts)
239
239 - def _preprocess_hf_mm_data(
240 + def _get_hf_mm_inputs(
241 self,
241 - mm_data: Mapping[str, object],
242 - hf_processor_mm_kwargs: Mapping[str, object],
243 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
244 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
245 -
246 - mm_data = dict(mm_data)
247 - audios = mm_data.pop("audios", [])
248 - if audios:
249 - mm_data["audio"] = audios
250 -
251 - hf_processor_mm_kwargs = dict(
252 - **hf_processor_mm_kwargs,
253 - sampling_rate=feature_extractor.sampling_rate,
254 - truncation=True,
242 + mm_items: MultiModalDataItems,
243 + hf_kwargs: Mapping[str, object],
244 + ) -> HFMultiModalInputs:
245 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
246 +
247 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
248 + return hf_inputs._replace(
249 + hf_kwargs=dict(
250 + hf_inputs.hf_kwargs,
251 + sampling_rate=feature_extractor.sampling_rate,
252 + truncation=True,
253 + )
254 )
255
257 - return mm_data, hf_processor_mm_kwargs
258 -
256 def _get_mm_fields_config(
257 self,
258 hf_inputs: BatchFeature,
vllm/model_executor/models/qwen3_asr_realtime.py
+5 -6
@@ -37,11 +37,10 @@ from vllm.model_executor.models.qwen3_asr import (
37 )
38 from vllm.multimodal import MULTIMODAL_REGISTRY
39 from vllm.multimodal.cache import _I, BaseMultiModalProcessorCache
40 -from vllm.multimodal.inputs import MultiModalKwargsOptionalItems
40 from vllm.multimodal.parse import MultiModalDataItems
41 from vllm.multimodal.processing.processor import (
42 BaseDummyInputsBuilder,
44 - MultiModalPromptUpdates,
43 + MultiModalProcessingResult,
44 PlaceholderFeaturesInfo,
45 cached_encode,
46 )
@@ -114,10 +113,10 @@ class Qwen3ASRRealtimeMultiModalProcessor(Qwen3ASRMultiModalProcessor):
113 def _maybe_apply_prompt_updates(
114 self,
115 mm_items: MultiModalDataItems,
117 - prompt_ids: list[int],
118 - mm_kwargs: MultiModalKwargsOptionalItems,
119 - mm_prompt_updates: MultiModalPromptUpdates,
116 + mm_res: MultiModalProcessingResult,
117 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
118 + mm_kwargs = mm_res.kwargs
119 +
120 audios = mm_kwargs.get("audio", [])
121 assert len(audios) == 1, (
122 f"Expected only one audio input for realtime, got {len(audios)}"
@@ -147,7 +146,7 @@ class Qwen3ASRRealtimeMultiModalProcessor(Qwen3ASRMultiModalProcessor):
146 # Find the audio_pad token position and expand it to audio_len tokens
147 expanded_ids = list[int]()
148 pad_start_idx = -1
150 - for i, tid in enumerate(prompt_ids):
149 + for i, tid in enumerate(mm_res.prompt_ids):
150 if tid == audio_pad_id and pad_start_idx == -1:
151 pad_start_idx = i
152 expanded_ids.extend([audio_pad_id] * audio_len)
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+32 -32
@@ -73,6 +73,8 @@ from vllm.multimodal.inputs import (
73 )
74 from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems
75 from vllm.multimodal.processing.processor import (
76 + HFMultiModalInputs,
77 + MultiModalProcessingResult,
78 MultiModalPromptUpdates,
79 PlaceholderFeaturesInfo,
80 PromptReplacement,
@@ -1219,13 +1221,26 @@ Qwen3OmniMoeThinkerDummyInputsBuilder = Qwen2_5OmniThinkerDummyInputsBuilder
1221 class Qwen3OmniMoeThinkerMultiModalProcessor(
1222 Qwen2_5OmniThinkerMultiModalProcessor,
1223 ):
1222 - def _get_hf_mm_data(
1224 + def _get_hf_mm_inputs(
1225 self,
1226 mm_items: MultiModalDataItems,
1225 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1226 - processor_data, passthrough_data = super()._get_hf_mm_data(mm_items)
1227 + hf_kwargs: Mapping[str, object],
1228 + ) -> HFMultiModalInputs:
1229 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
1230
1228 - audios = processor_data.get("audios")
1231 + if mm_items.get_count("audio", strict=False):
1232 + # TODO(Isotr0py): Remove this patch after upstream fix PR
1233 + # released and Transformers version update:
1234 + # https://github.com/huggingface/transformers/pull/41473
1235 + hf_inputs = hf_inputs._replace(
1236 + hf_kwargs={
1237 + **hf_inputs.hf_kwargs,
1238 + "audio_kwargs": dict(hf_inputs.hf_kwargs.get("audio_kwargs") or {}),
1239 + "text_kwargs": dict(hf_inputs.hf_kwargs.get("text_kwargs") or {}),
1240 + }
1241 + )
1242 +
1243 + audios = hf_inputs.hf_data.get("audio")
1244 if audios:
1245 feature_extractor = self.info.get_feature_extractor()
1246 hop_length = feature_extractor.hop_length
@@ -1239,47 +1254,30 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
1254
1255 # To make sure the cache works with padding=True, we pre-padded
1256 # the audio to a multiple of hop_length.
1242 - processor_data = dict(processor_data)
1243 - processor_data["audios"] = [
1257 + hf_inputs.hf_data["audio"] = [
1258 pad_to_hop_length(audio)
1259 if isinstance(audio, np.ndarray)
1260 else (pad_to_hop_length(audio[0]), audio[1])
1261 for audio in audios
1262 ]
1263
1250 - return processor_data, passthrough_data
1264 + return hf_inputs
1265
1252 - def _apply_hf_processor_main(
1266 + def _postprocess_hf_mm_data(
1267 self,
1254 - mm_items: MultiModalDataItems,
1268 + mm_data: Mapping[str, object],
1269 hf_processor_mm_kwargs: Mapping[str, object],
1270 + processed_data: BatchFeature,
1271 ) -> BatchFeature:
1257 - if mm_items.get_all_counts().get("audio", 0):
1258 - # TODO(Isotr0py): Remove this patch after upstream fix PR
1259 - # released and Transformers version update:
1260 - # https://github.com/huggingface/transformers/pull/41473
1261 - hf_processor_mm_kwargs = dict(hf_processor_mm_kwargs)
1262 - hf_processor_mm_kwargs["audio_kwargs"] = dict(
1263 - hf_processor_mm_kwargs.get("audio_kwargs") or {}
1264 - )
1265 - hf_processor_mm_kwargs["text_kwargs"] = dict(
1266 - hf_processor_mm_kwargs.get("text_kwargs") or {}
1267 - )
1268 -
1269 - processed_data = super()._apply_hf_processor_main(
1270 - mm_items,
1271 - hf_processor_mm_kwargs,
1272 + processed_data = super()._postprocess_hf_mm_data(
1273 + mm_data, hf_processor_mm_kwargs, processed_data
1274 )
1275
1276 if (
1277 "audio_feature_lengths" in processed_data
1278 and "feature_attention_mask" in processed_data
1279 ):
1278 - valid_mm_items = mm_items.select(
1279 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1280 - )
1281 - processor_data, _ = self._get_hf_mm_data(valid_mm_items)
1282 - audios = processor_data.get("audios", [])
1280 + audios = mm_data.get("audio", [])
1281 if audios:
1282 feature_extractor = self.info.get_feature_extractor(
1283 **hf_processor_mm_kwargs
@@ -1312,12 +1310,14 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
1310 def _maybe_apply_prompt_updates(
1311 self,
1312 mm_items: MultiModalDataItems,
1315 - prompt_ids: list[int],
1316 - mm_kwargs: MultiModalKwargsItems,
1317 - mm_prompt_updates: MultiModalPromptUpdates,
1313 + mm_res: MultiModalProcessingResult,
1314 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
1315 """Qwen3-Omni reimplements this function to handle `use_audio_in_video`."""
1316 mm_item_counts = mm_items.get_all_counts()
1317 + prompt_ids = mm_res.prompt_ids
1318 + mm_kwargs = mm_res.kwargs
1319 + mm_prompt_updates = mm_res.prompt_updates
1320 +
1321 self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
1322
1323 use_audio_in_video = False
vllm/model_executor/models/qwen3_vl.py
+16 -22
@@ -1327,27 +1327,25 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1327 proc_impl = getattr(type(hf_processor), "replace_video_token", None)
1328 return proc_impl is not None and proc_impl is not mixin_impl
1329
1330 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1331 + return self.dummy_inputs.get_dummy_text(mm_counts)
1332 +
1333 def _apply_hf_processor_main(
1334 self,
1335 mm_items: MultiModalDataItems,
1333 - hf_processor_mm_kwargs: Mapping[str, object],
1336 + hf_kwargs: Mapping[str, object],
1337 ) -> BatchFeature:
1335 - valid_mm_items = mm_items.select(
1336 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1338 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1339 + mm_items, hf_kwargs
1340 )
1338 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1339 -
1340 - if not mm_data:
1341 - return BatchFeature(dict(passthrough_data))
1342 -
1343 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
1341
1345 - mm_data = dict(mm_data)
1342 + if not hf_data:
1343 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1344
1345 # Separate video processing from image processing. Because the videos
1346 # are processed into several image patches
1347 video_input_ids_lst: list[list[int]] = []
1350 - if videos := mm_data.pop("videos", []):
1348 + if videos := hf_data.pop("videos", []):
1349 video_grid_thw_lst = []
1350 pixel_values_videos_lst = []
1351 timestamps_per_video = []
@@ -1370,10 +1368,8 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1368
1369 # NOTE: a copy of is created to update do_sample_frames,
1370 # otherwise mm_hash for the object will be incorrect.
1373 - video_mm_kwargs = dict(**hf_processor_mm_kwargs)
1374 - merged = self.info.ctx.get_merged_mm_kwargs(
1375 - hf_processor_mm_kwargs, modality="video"
1376 - )
1371 + video_mm_kwargs = dict(**hf_kwargs)
1372 + merged = self.info.ctx.get_merged_mm_kwargs(hf_kwargs, modality="video")
1373 if merged.keys() & {"size", "min_pixels", "max_pixels"}:
1374 video_size = dict(self.info.get_video_processor().size)
1375 size_override = merged.get("size")
@@ -1485,13 +1481,11 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1481 # fps/num_frames are video-only kwargs already consumed by the loop;
1482 # exclude them so the text/image processor call below never gets a list.
1483 non_video_mm_kwargs = {
1488 - k: v
1489 - for k, v in hf_processor_mm_kwargs.items()
1490 - if k not in ("fps", "num_frames")
1484 + k: v for k, v in hf_kwargs.items() if k not in ("fps", "num_frames")
1485 }
1486 processed_data = self.info.ctx.call_hf_processor(
1487 self.info.get_hf_processor(**non_video_mm_kwargs),
1494 - dict(text=prompt_text, **mm_data),
1488 + hf_data,
1489 non_video_mm_kwargs,
1490 )
1491
@@ -1516,9 +1510,9 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1510 processed_data["input_ids"] = [expanded_ids]
1511
1512 processed_data.update(video_outputs)
1519 - processed_data.update(passthrough_data)
1520 -
1521 - return processed_data
1513 + return self._finalize_hf_mm_data(
1514 + hf_data, hf_kwargs, passthrough_data, processed_data
1515 + )
1516
1517 def _get_mm_fields_config(
1518 self,
vllm/model_executor/models/terratorch.py
+3 -1
@@ -202,7 +202,9 @@ class TerratorchMultiModalProcessor(BaseMultiModalProcessor[TerratorchProcessing
202 hf_processor_mm_kwargs = inputs.hf_processor_mm_kwargs
203
204 with timing_ctx.record("apply_hf_processor"):
205 - _, passthrough_data = self._get_hf_mm_data(mm_items)
205 + passthrough_data = self._get_hf_mm_inputs(
206 + mm_items, hf_processor_mm_kwargs
207 + ).passthrough_data
208 mm_processed_data = BatchFeature(
209 {
210 k: torch.as_tensor(v).unsqueeze(0)
vllm/model_executor/models/transformers/multimodal.py
+44 -35
@@ -219,18 +219,6 @@ class _MultiModalProcessorBase(BaseMultiModalProcessor[MultiModalProcessingInfo]
219 Subclasses add the strategy for locating placeholders in the prompt.
220 """
221
222 - def _get_hf_mm_data(
223 - self,
224 - mm_items: MultiModalDataItems,
225 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
226 - """Rename the parser's `audios` key to the `audio` argument HF audio
227 - processors take.
228 - """
229 - processor_data, passthrough_data = super()._get_hf_mm_data(mm_items)
230 - if self.info._is_audio_model() and "audios" in processor_data:
231 - processor_data["audio"] = processor_data.pop("audios")
232 - return processor_data, passthrough_data
233 -
222 def _get_modality_field_names(self, modality: str) -> set[str]:
223 """Names of the fields the sub-processor for `modality` produces."""
224 # TODO: use else branch only once huggingface/transformers#44394 lands.
@@ -353,6 +341,9 @@ class _MultiModalProcessorBase(BaseMultiModalProcessor[MultiModalProcessingInfo]
341
342 return mm_fields
343
344 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
345 + return self.dummy_inputs.get_dummy_text(mm_counts)
346 +
347 def _unpad_audios(
348 self,
349 hf_inputs: "BatchFeature",
@@ -597,21 +588,31 @@ class LegacyMultiModalProcessor(_MultiModalProcessorBase):
588 # work for Transformers. The vision path has logic tied to
589 # `mm_tokens_per_modality` in _apply_vision()
590 hf_processor_mm_kwargs = {
600 - **hf_processor_mm_kwargs,
591 + # vLLM needs the untruncated sequence to keep placeholder
592 + # tokens aligned. Note that the text inputs are just dummy
593 + # text, not the original prompt. The original prompt is
594 + # already tokenized by the renderer.
595 + "truncation": False,
596 # HF processors only accept text, and the decoded string already
597 # contains any special tokens, so don't let them be added again
598 # (the HF processor call disables `add_special_tokens`).
599 "add_special_tokens": False,
600 + **hf_processor_mm_kwargs,
601 }
602
607 - valid_mm_items = mm_items.select(
608 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
603 + processor_data, _, passthrough_data = self._get_hf_mm_inputs(
604 + mm_items, hf_processor_mm_kwargs
605 )
610 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
606 +
607 + # The real prompt is fed to the processor below and the placeholder
608 + # ranges are read back out of its output, so the dummy text meant for
609 + # media-only callers is discarded here.
610 + processor_data.pop("text", None)
611 + has_mm_data = any(processor_data.values())
612
613 # Ask which modality owns each token of the expanded prompt, which
614 # is the only marking of the tokens an expansion adds around an item
614 - if any(processor_data.values()):
615 + if has_mm_data:
616 processor_data = {**processor_data, "return_mm_token_type_ids": True}
617
618 try:
@@ -621,7 +622,7 @@ class LegacyMultiModalProcessor(_MultiModalProcessorBase):
622 hf_processor_mm_kwargs,
623 )
624 except ValueError:
624 - if any(processor_data.values()):
625 + if has_mm_data:
626 raise
627 # Some processors reject a prompt holding placeholders with
628 # no data to go with them, so tokenize it without them
@@ -792,31 +793,37 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
793 except (AttributeError, KeyError, TypeError):
794 return None
795
796 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
797 + return self.dummy_inputs.get_dummy_text(mm_counts)
798 +
799 def _apply_hf_processor_main(
800 self,
801 mm_items: MultiModalDataItems,
798 - hf_processor_mm_kwargs: Mapping[str, object],
802 + hf_kwargs: Mapping[str, object],
803 ) -> "BatchFeature":
800 - mm_counts = mm_items.get_all_counts()
804 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
805 + mm_items, hf_kwargs
806 + )
807
802 - valid_mm_items = mm_items.select({k for k, c in mm_counts.items() if c > 0})
803 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
808 + if not hf_data:
809 + return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
810
805 - prompt_text = self.dummy_inputs.get_dummy_text(mm_counts)
811 + prompt_text = hf_data.pop("text")
812 + assert isinstance(prompt_text, str)
813
814 # Ask for the replacement each placeholder expands to, and record it as
815 # per-item fields: its token ids, and the tokens or patches behind them
809 - if has_mm_data := any(mm_data.values()):
810 - mm_data = {**mm_data, "return_text_replacement_offsets": True}
816 + if has_mm_data := any(hf_data.values()):
817 + hf_data = {**hf_data, "return_text_replacement_offsets": True}
818
819 try:
820 hf_inputs = self.info.ctx.call_hf_processor(
814 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
815 - dict(text=prompt_text, **mm_data),
816 - hf_processor_mm_kwargs,
821 + self.info.get_hf_processor(**hf_kwargs),
822 + dict(text=prompt_text, **hf_data),
823 + hf_kwargs,
824 )
825 except ValueError:
819 - if any(mm_data.values()):
826 + if any(hf_data.values()):
827 raise
828 # Some processors reject a prompt holding placeholders with
829 # no data to go with them, so tokenize it without them
@@ -825,7 +832,7 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
832 dict(input_ids=[tokenizer.encode(prompt_text)]), tensor_type="pt"
833 )
834 self._unpad_images(hf_inputs)
828 - self._unpad_audios(hf_inputs, mm_data, hf_processor_mm_kwargs)
835 + self._unpad_audios(hf_inputs, hf_data, hf_kwargs)
836
837 # Drop the inputs the model would reject
838 hf_inputs.pop("mm_token_type_ids", None)
@@ -845,9 +852,10 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
852 "so it can be fixed, and install `transformers<5.15.0` in the "
853 "meantime to locate placeholders in the expanded prompt instead."
854 )
848 - hf_inputs.update(passthrough_data)
855 hf_inputs.pop("input_ids", None)
850 - return hf_inputs
856 + return self._finalize_hf_mm_data(
857 + hf_data, hf_kwargs, passthrough_data, hf_inputs
858 + )
859
860 tokenizer = self.info.get_tokenizer()
861 replacements = defaultdict[str, list[list[int]]](list)
@@ -865,7 +873,7 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
873 )
874 if modality == "image":
875 hf_inputs["num_image_patches"] = self._get_num_image_patches(
868 - hf_inputs, mm_data, len(seqs)
876 + hf_inputs, hf_data, len(seqs)
877 )
878 elif modality == "audio":
879 counts = []
@@ -874,10 +882,11 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
882 counts.append(int(ids.eq(_get_embed_token_id(ids)).sum()))
883 hf_inputs["num_audio_tokens"] = torch.tensor(counts)
884
877 - hf_inputs.update(passthrough_data)
885 hf_inputs.pop("input_ids")
886
880 - return hf_inputs
887 + return self._finalize_hf_mm_data(
888 + hf_data, hf_kwargs, passthrough_data, hf_inputs
889 + )
890
891
892 # From this version on, a processor reporting no offsets is an error rather than a
vllm/model_executor/models/ultravox.py
+22 -61
@@ -42,6 +42,7 @@ from vllm.multimodal.processing import (
42 PromptUpdate,
43 cached_encode,
44 )
45 +from vllm.multimodal.processing.processor import HFMultiModalInputs
46 from vllm.renderers import TokenizeParams
47 from vllm.sequence import IntermediateTensors
48 from vllm.transformers_utils.configs.ultravox import UltravoxConfig
@@ -190,72 +191,32 @@ class UltravoxDummyInputsBuilder(BaseDummyInputsBuilder[UltravoxProcessingInfo])
191
192
193 class UltravoxMultiModalProcessor(BaseMultiModalProcessor[UltravoxProcessingInfo]):
193 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
194 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
195 return self.dummy_inputs.get_dummy_text(mm_counts)
196
196 - def _preprocess_hf_mm_data(
197 - self,
198 - mm_data: Mapping[str, object],
199 - hf_processor_mm_kwargs: Mapping[str, object],
200 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
201 - audios = mm_data.get("audios", [])
202 - assert isinstance(audios, list)
203 -
204 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
205 - hf_processor_mm_kwargs = dict(
206 - **hf_processor_mm_kwargs,
207 - sampling_rate=feature_extractor.sampling_rate,
208 - include_audio_num_chunks=True,
209 - )
210 -
211 - return mm_data, hf_processor_mm_kwargs
212 -
213 - def _apply_hf_processor_main(
197 + def _get_hf_mm_inputs(
198 self,
199 mm_items: MultiModalDataItems,
216 - hf_processor_mm_kwargs: Mapping[str, object],
217 - ) -> BatchFeature:
218 - valid_mm_items = mm_items.select(
219 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
220 - )
221 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
222 -
223 - if processor_data:
224 - processor_data, hf_processor_mm_kwargs = self._preprocess_hf_mm_data(
225 - processor_data,
226 - hf_processor_mm_kwargs,
200 + hf_kwargs: Mapping[str, object],
201 + ) -> HFMultiModalInputs:
202 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
203 +
204 + # The Ultravox processor accepts "audios" instead of "audio"
205 + hf_data = hf_inputs.hf_data
206 + if "audio" in hf_data:
207 + hf_data["audios"] = hf_data.pop("audio")
208 +
209 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
210 + normalized_kwargs = dict(hf_inputs.hf_kwargs)
211 + # The remote processor already hardcodes truncation=False and forwards
212 + # its kwargs to the audio processor, where another value would collide.
213 + normalized_kwargs.pop("truncation", None)
214 + return hf_inputs._replace(
215 + hf_kwargs=dict(
216 + normalized_kwargs,
217 + sampling_rate=feature_extractor.sampling_rate,
218 + include_audio_num_chunks=True,
219 )
228 -
229 - prompt_text = self._get_hf_processor_text(mm_items.get_all_counts())
230 - if prompt_text is not None:
231 - processor_data = dict(text=prompt_text, **processor_data)
232 -
233 - hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
234 -
235 - def patched_call(**kwargs) -> BatchFeature:
236 - # The remote UltravoxProcessor does not support the
237 - # `truncation` kwarg injected by `call_hf_processor`: it
238 - # passes `truncation` to its audio processor by itself while
239 - # also forwarding `**kwargs` there, causing a duplicate
240 - # keyword argument error. Its hardcoded `truncation=False`
241 - # already matches vLLM's intended behavior.
242 - kwargs.pop("truncation", None)
243 -
244 - return hf_processor(**kwargs)
245 -
246 - processed_data = self.info.ctx.call_hf_processor(
247 - patched_call,
248 - processor_data,
249 - hf_processor_mm_kwargs,
250 - )
251 - processed_data.update(passthrough_data)
252 - else:
253 - processed_data = BatchFeature(dict(passthrough_data))
254 -
255 - return self._postprocess_hf_mm_data(
256 - processor_data,
257 - hf_processor_mm_kwargs,
258 - processed_data,
220 )
221
222 def _postprocess_hf_mm_data(
vllm/model_executor/models/unlimited_ocr.py
+2 -2
@@ -75,7 +75,7 @@ from vllm.multimodal.parse import (
75 from vllm.multimodal.processing import PromptReplacement, PromptUpdate
76 from vllm.multimodal.processing.context import TimingContext
77 from vllm.multimodal.processing.inputs import ProcessorInputs
78 -from vllm.multimodal.processing.processor import MultiModalProcessingInfo
78 +from vllm.multimodal.processing.processor import MultiModalProcessingResult
79 from vllm.transformers_utils.processors.deepseek_ocr import (
80 BASE_SIZE,
81 CROP_MODE,
@@ -225,7 +225,7 @@ class UnlimitedOCRMultiModalProcessor(DeepseekOCRMultiModalProcessor):
225 self,
226 inputs: ProcessorInputs,
227 timing_ctx: TimingContext,
228 - ) -> MultiModalProcessingInfo:
228 + ) -> MultiModalProcessingResult:
229 # The processor logic differs for single-image (crop) vs multi-image
230 # (no crop) requests. The processing cache assumes per-item output is
231 # invariant of how many images are passed per prompt, so we only cache
vllm/model_executor/models/voxtral.py
+18 -39
@@ -16,7 +16,7 @@ from mistral_common.protocol.instruct.messages import UserMessage
16 from mistral_common.protocol.instruct.request import ChatCompletionRequest
17 from mistral_common.protocol.transcription.request import TranscriptionRequest
18 from mistral_common.tokens.tokenizers.audio import Audio
19 -from transformers import BatchFeature, WhisperConfig
19 +from transformers import WhisperConfig
20
21 from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
22 from vllm.config.multimodal import BaseDummyOptions
@@ -36,7 +36,6 @@ from vllm.multimodal import MULTIMODAL_REGISTRY
36 from vllm.multimodal.inputs import (
37 MultiModalFieldConfig,
38 MultiModalKwargsItems,
39 - MultiModalKwargsOptionalItems,
39 NestedTensors,
40 )
41 from vllm.multimodal.parse import (
@@ -48,7 +47,8 @@ from vllm.multimodal.processing import BaseDummyInputsBuilder
47 from vllm.multimodal.processing.processor import (
48 BaseMultiModalProcessor,
49 BaseProcessingInfo,
51 - MultiModalPromptUpdates,
50 + HFMultiModalInputs,
51 + MultiModalProcessingResult,
52 PlaceholderFeaturesInfo,
53 ProcessorInputs,
54 PromptReplacement,
@@ -61,7 +61,6 @@ from vllm.transformers_utils.processors.voxtral import (
61 MistralCommonFeatureExtractor,
62 MistralCommonVoxtralProcessor,
63 )
64 -from vllm.utils.collection_utils import is_list_of
64
65 from .interfaces import SupportsLoRA, SupportsMultiModal, SupportsTranscription
66 from .utils import init_vllm_registered_model, maybe_prefix
@@ -209,21 +208,19 @@ class VoxtralMultiModalProcessor(BaseMultiModalProcessor[VoxtralProcessingInfo])
208 def _maybe_apply_prompt_updates(
209 self,
210 mm_items: MultiModalDataItems,
212 - prompt_ids: list[int],
213 - mm_kwargs: MultiModalKwargsOptionalItems,
214 - mm_prompt_updates: MultiModalPromptUpdates,
211 + mm_res: MultiModalProcessingResult,
212 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
213 mm_item_counts = mm_items.get_all_counts()
217 - self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
218 - self._validate_mm_updates(mm_prompt_updates, mm_item_counts)
214 + self._validate_mm_kwargs(mm_res.kwargs, mm_item_counts)
215 + self._validate_mm_updates(mm_res.prompt_updates, mm_item_counts)
216
217 mm_placeholders = self._find_mm_placeholders(
221 - prompt_ids,
222 - mm_prompt_updates,
218 + mm_res.prompt_ids,
219 + mm_res.prompt_updates,
220 )
221 self._validate_mm_placeholders(mm_placeholders, mm_item_counts)
222
226 - return prompt_ids, mm_placeholders
223 + return mm_res.prompt_ids, mm_placeholders
224
225 def _get_mm_fields_config(
226 self,
@@ -241,38 +238,20 @@ class VoxtralMultiModalProcessor(BaseMultiModalProcessor[VoxtralProcessingInfo])
238 # skip validation here
239 pass
240
244 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
241 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
242 return self.dummy_inputs.get_dummy_text(mm_counts)
243
247 - def _preprocess_hf_mm_data(
244 + def _get_hf_mm_inputs(
245 self,
249 - mm_data: Mapping[str, object],
250 - hf_processor_mm_kwargs: Mapping[str, object],
251 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
252 - mm_data = dict(mm_data)
253 - audios = mm_data.pop("audios", [])
254 -
255 - if audios:
256 - # MistralCommonVoxtralProcessor accepts "audio"
257 - mm_data["audio"] = audios
246 + mm_items: MultiModalDataItems,
247 + hf_kwargs: Mapping[str, object],
248 + ) -> HFMultiModalInputs:
249 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
250
251 # Avoid padding issue
260 - hf_processor_mm_kwargs = dict(**hf_processor_mm_kwargs, return_tensors=None)
261 -
262 - return mm_data, hf_processor_mm_kwargs
263 -
264 - def _postprocess_hf_mm_data(
265 - self,
266 - mm_data: Mapping[str, object],
267 - hf_processor_mm_kwargs: Mapping[str, object],
268 - processed_data: BatchFeature,
269 - ) -> BatchFeature:
270 - # Missing batch dimension
271 - input_ids = processed_data.get("input_ids")
272 - if is_list_of(input_ids, int):
273 - processed_data["input_ids"] = [input_ids]
274 -
275 - return processed_data
252 + return hf_inputs._replace(
253 + hf_kwargs=dict(hf_inputs.hf_kwargs, return_tensors=None)
254 + )
255
256 def _get_prompt_updates(
257 self,
vllm/model_executor/models/voxtral_realtime.py
+5 -6
@@ -28,11 +28,10 @@ from vllm.model_executor.models.voxtral import (
28 )
29 from vllm.multimodal import MULTIMODAL_REGISTRY
30 from vllm.multimodal.cache import _I, BaseMultiModalProcessorCache
31 -from vllm.multimodal.inputs import MultiModalKwargsOptionalItems
31 from vllm.multimodal.parse import MultiModalDataItems
32 from vllm.multimodal.processing import BaseDummyInputsBuilder
33 from vllm.multimodal.processing.processor import (
35 - MultiModalPromptUpdates,
34 + MultiModalProcessingResult,
35 PlaceholderFeaturesInfo,
36 )
37 from vllm.sequence import IntermediateTensors
@@ -60,10 +59,10 @@ class VoxtralRealtimeMultiModalProcessor(VoxtralMultiModalProcessor):
59 def _maybe_apply_prompt_updates(
60 self,
61 mm_items: MultiModalDataItems,
63 - prompt_ids: list[int],
64 - mm_kwargs: MultiModalKwargsOptionalItems,
65 - mm_prompt_updates: MultiModalPromptUpdates,
62 + mm_res: MultiModalProcessingResult,
63 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
64 + mm_kwargs = mm_res.kwargs
65 +
66 # there are no placeholder audio tokens for streaming
67 # so we need to build the place placeholder positions manually
68
@@ -86,7 +85,7 @@ class VoxtralRealtimeMultiModalProcessor(VoxtralMultiModalProcessor):
85 * [0], # only used for length computation, so we can take dummy inputs
86 is_embed=None,
87 )
89 - return prompt_ids, {"audio": [features_info]}
88 + return mm_res.prompt_ids, {"audio": [features_info]}
89
90
91 class TimeEmbedding(torch.nn.Module):
vllm/model_executor/models/whisper.py
+16 -28
@@ -61,6 +61,7 @@ from vllm.multimodal.processing import (
61 PromptReplacement,
62 PromptUpdate,
63 )
64 +from vllm.multimodal.processing.processor import HFMultiModalInputs
65 from vllm.renderers import TokenizeParams
66 from vllm.transformers_utils.processor import cached_processor_from_config
67 from vllm.utils.jsontree import json_map_leaves
@@ -743,6 +744,9 @@ class WhisperDummyInputsBuilder(BaseDummyInputsBuilder[WhisperProcessingInfo]):
744
745
746 class WhisperMultiModalProcessor(EncDecMultiModalProcessor[WhisperProcessingInfo]):
747 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
748 + return self.dummy_inputs.get_dummy_text(mm_counts)
749 +
750 def create_encoder_prompt(
751 self,
752 prompt: list[int],
@@ -754,37 +758,21 @@ class WhisperMultiModalProcessor(EncDecMultiModalProcessor[WhisperProcessingInfo
758 # for encoder profiling.
759 return [0]
760
757 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
758 - return self.dummy_inputs.get_dummy_text(mm_counts)
759 -
760 - def _preprocess_hf_mm_data(
761 + def _get_hf_mm_inputs(
762 self,
762 - mm_data: Mapping[str, object],
763 - hf_processor_mm_kwargs: Mapping[str, object],
764 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
765 - feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
766 -
767 - mm_data = dict(mm_data)
768 - mm_data["audio"] = mm_data.pop("audios")
769 -
770 - hf_processor_mm_kwargs = dict(
771 - **hf_processor_mm_kwargs,
772 - sampling_rate=feature_extractor.sampling_rate,
763 + mm_items: MultiModalDataItems,
764 + hf_kwargs: Mapping[str, object],
765 + ) -> HFMultiModalInputs:
766 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
767 +
768 + feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
769 + return hf_inputs._replace(
770 + hf_kwargs=dict(
771 + hf_inputs.hf_kwargs,
772 + sampling_rate=feature_extractor.sampling_rate,
773 + )
774 )
775
775 - return mm_data, hf_processor_mm_kwargs
776 -
777 - def _postprocess_hf_mm_data(
778 - self,
779 - mm_data: Mapping[str, object],
780 - hf_processor_mm_kwargs: Mapping[str, object],
781 - processed_data: BatchFeature,
782 - ) -> BatchFeature:
783 - if "labels" in processed_data:
784 - processed_data["input_ids"] = processed_data.pop("labels")
785 -
786 - return processed_data
787 -
776 def _get_mm_fields_config(
777 self,
778 hf_inputs: BatchFeature,
vllm/models/dots3_note/common/processor.py
+26 -18
@@ -34,6 +34,7 @@ from vllm.multimodal.processing import (
34 PromptUpdate,
35 PromptUpdateDetails,
36 )
37 +from vllm.multimodal.processing.processor import HFMultiModalInputs
38 from vllm.transformers_utils.repo_utils import get_hf_file_to_dict
39
40 from .video import preprocess_dots3_note_video
@@ -630,26 +631,33 @@ class Dots3NoteDummyInputsBuilder(BaseDummyInputsBuilder[Dots3NoteProcessingInfo
631
632
633 class Dots3NoteMultiModalProcessor(BaseMultiModalProcessor[Dots3NoteProcessingInfo]):
633 - def _get_hf_mm_data(
634 + def _get_hf_mm_inputs(
635 self,
636 mm_items: MultiModalDataItems,
636 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
637 - processor_data, passthrough_data = super()._get_hf_mm_data(mm_items)
638 - if "video" not in mm_items:
639 - return processor_data, passthrough_data
640 -
641 - videos = mm_items.get_items("video", VideoProcessorItems)
642 - raw_videos: list[object] = []
643 - for index, item in enumerate(videos.data):
644 - if isinstance(item, MediaWithBytes):
645 - raw_videos.append(item.original_bytes)
646 - else:
647 - raw_videos.append(videos.get(index))
648 - processor_data = dict(processor_data)
649 - processor_data["videos"] = raw_videos
650 - return processor_data, passthrough_data
651 -
652 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
637 + hf_kwargs: Mapping[str, object],
638 + ) -> HFMultiModalInputs:
639 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
640 +
641 + # The Dots3Note processor accepts "audios" instead of "audio"
642 + hf_data = hf_inputs.hf_data
643 + if "audio" in hf_data:
644 + hf_data["audios"] = hf_data.pop("audio")
645 +
646 + if "video" in mm_items:
647 + videos = mm_items.get_items("video", VideoProcessorItems)
648 +
649 + raw_videos: list[object] = []
650 + for index, item in enumerate(videos.data):
651 + if isinstance(item, MediaWithBytes):
652 + raw_videos.append(item.original_bytes)
653 + else:
654 + raw_videos.append(videos.get(index))
655 +
656 + hf_data["videos"] = raw_videos
657 +
658 + return hf_inputs
659 +
660 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
661 return self.dummy_inputs.get_dummy_text(mm_counts)
662
663 def _get_mm_fields_config(
vllm/models/inkling/common/mm_preprocess.py
+8 -8
@@ -182,20 +182,19 @@ class InklingMultiModalProcessor(BaseMultiModalProcessor[InklingProcessingInfo])
182 def _apply_hf_processor_main(
183 self,
184 mm_items: MultiModalDataItems,
185 - hf_processor_mm_kwargs: Mapping[str, object],
185 + hf_kwargs: Mapping[str, object],
186 ) -> BatchFeature:
187 - valid_mm_items = mm_items.select(
188 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
187 + mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
188 + mm_items, hf_kwargs
189 )
190 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
190
191 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
192
194 - processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
193 + processor = self.info.get_hf_processor(**hf_kwargs)
194 tokenizer = self.info.get_tokenizer()
195
196 images = mm_data.get("images") or []
198 - audios = mm_data.get("audios") or []
197 + audios = mm_data.get("audio") or []
198 if not isinstance(images, list):
199 images = list(cast(Iterable[Any], images))
200 if not isinstance(audios, list):
@@ -235,8 +234,9 @@ class InklingMultiModalProcessor(BaseMultiModalProcessor[InklingProcessingInfo])
234 data["num_audio_tokens"] = torch.tensor(num_audio_tokens, dtype=torch.int64)
235
236 processed_data = BatchFeature(data=data, tensor_type=None)
238 - processed_data.update(passthrough_data)
239 - return processed_data
237 + return self._finalize_hf_mm_data(
238 + mm_data, hf_kwargs, passthrough_data, processed_data
239 + )
240
241 def _tokenize_with_placeholders(
242 self,
vllm/models/minimax_m3/common/mm_preprocess.py
+25 -25
@@ -32,6 +32,7 @@ from vllm.multimodal.processing import (
32 PromptUpdate,
33 PromptUpdateDetails,
34 )
35 +from vllm.multimodal.processing.processor import HFMultiModalInputs
36 from vllm.multimodal.video import (
37 VIDEO_LOADER_REGISTRY,
38 VideoBackend,
@@ -307,26 +308,21 @@ class MiniMaxM3VLDummyInputsBuilder(BaseDummyInputsBuilder[MiniMaxM3VLProcessing
308 class MiniMaxM3VLMultiModalProcessor(
309 BaseMultiModalProcessor[MiniMaxM3VLProcessingInfo]
310 ):
310 - def _apply_hf_processor_main(
311 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
312 + return self.dummy_inputs.get_dummy_text(mm_counts)
313 +
314 + def _get_hf_mm_inputs(
315 self,
316 mm_items: MultiModalDataItems,
313 - hf_processor_mm_kwargs: Mapping[str, object],
314 - ) -> BatchFeature:
315 - valid_mm_items = mm_items.select(
316 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
317 - )
318 - mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
319 -
320 - if not mm_data:
321 - return BatchFeature(dict(passthrough_data))
322 -
323 - prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
317 + hf_kwargs: Mapping[str, object],
318 + ) -> HFMultiModalInputs:
319 + hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
320 + hf_data = hf_inputs.hf_data
321
325 - mm_data = dict(mm_data)
322 # With ``video_needs_metadata=True`` each video arrives as a
323 # ``(frames, metadata)`` tuple. Split the frames back out and forward the
324 # metadata as ``VideoMetadata`` so the processor emits timestamps.
329 - videos = cast(list | None, mm_data.get("videos"))
325 + videos = cast(list | None, hf_data.get("videos"))
326 video_metadata: list[VideoMetadata] | None = None
327 if videos:
328 frames_only = []
@@ -346,21 +342,25 @@ class MiniMaxM3VLMultiModalProcessor(
342 # stays consistent with _get_prompt_updates.
343 meta.setdefault("total_num_frames", len(frames))
344 video_metadata.append(VideoMetadata(**meta))
349 - mm_data["videos"] = frames_only
345 + hf_data["videos"] = frames_only
346
351 - # Override the video processor's default do_resize=False (set for a
352 - # pre-resized pipeline) to True for vLLM's raw-frame inputs.
353 - merged = dict(do_resize=True, **hf_processor_mm_kwargs)
354 - data = dict(text=prompt_text, **mm_data)
347 if video_metadata is not None:
356 - data["video_metadata"] = video_metadata
357 - processed_data = self.info.ctx.call_hf_processor(
358 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
359 - data,
348 + hf_data["video_metadata"] = video_metadata
349 +
350 + return hf_inputs
351 +
352 + def _call_hf_processor(
353 + self,
354 + hf_data: Mapping[str, object],
355 + hf_kwargs: Mapping[str, object],
356 + ) -> BatchFeature:
357 + # Override the video processor's default for vLLM's raw-frame inputs.
358 + merged = dict(do_resize=True, **hf_kwargs)
359 + return self.info.ctx.call_hf_processor(
360 + self.info.get_hf_processor(**hf_kwargs),
361 + hf_data,
362 merged,
363 )
362 - processed_data.update(passthrough_data)
363 - return processed_data
364
365 def _get_mm_fields_config(
366 self,
vllm/multimodal/processing/context.py
+2 -8
@@ -321,12 +321,6 @@ class InputProcessingContext:
321
322 merged_kwargs = self.get_merged_mm_kwargs(kwargs)
323
324 - # vLLM needs the full untruncated sequence to keep multi-modal
325 - # placeholder tokens aligned; note that the text inputs in
326 - # call_hf_processor are just dummy text, not the original prompt.
327 - # The original prompt is already tokenized by the renderer.
328 - merged_kwargs.setdefault("truncation", False)
329 -
324 allowed_kwargs = get_allowed_kwarg_only_overrides(
325 hf_processor,
326 merged_kwargs,
@@ -426,7 +420,7 @@ class BaseProcessingInfo:
420 def get_data_parser(self) -> MultiModalDataParser:
421 """Constructs a parser to preprocess multi-modal data items
422 before passing them to
429 - [`_get_hf_mm_data`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_data].
423 + [`_get_hf_mm_inputs`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs].
424
425 You can support additional modalities by creating a subclass
426 of [`MultiModalDataParser`][vllm.multimodal.parse.MultiModalDataParser]
@@ -506,7 +500,7 @@ class BaseProcessingInfo:
500 """Normalize [`MultiModalDataDict`][vllm.inputs.MultiModalDataDict]
501 to [`MultiModalDataItems`][vllm.multimodal.parse.MultiModalDataItems]
502 before passing them to
509 - [`_get_hf_mm_data`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_data].
503 + [`_get_hf_mm_inputs`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs].
504 """
505 mm_items = self.data_parser.parse_mm_data(mm_data)
506
vllm/multimodal/processing/processor.py
+117 -85
@@ -1008,7 +1008,20 @@ For an item `MultiModalPromptUpdates[k][i]`,
1008 _I = TypeVar("_I", bound=BaseProcessingInfo)
1009
1010
1011 -class MultiModalProcessingInfo(NamedTuple):
1011 +class HFMultiModalInputs(NamedTuple):
1012 + hf_data: dict[str, object]
1013 + """The data inputs to the HF processor."""
1014 +
1015 + hf_kwargs: Mapping[str, object]
1016 + """The non-data inputs to the HF processor."""
1017 +
1018 + passthrough_data: dict[str, object]
1019 + """The inputs to assign to the output of the HF processor,
1020 + instead of passing them to the HF processor."""
1021 +
1022 +
1023 +class MultiModalProcessingResult(NamedTuple):
1024 + prompt_ids: list[int]
1025 kwargs: MultiModalKwargsOptionalItems
1026 hashes: MultiModalHashes
1027 prompt_updates: MultiModalPromptUpdates
@@ -1137,23 +1150,8 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1150 ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
1151 return find_mm_placeholders(new_token_ids, mm_prompt_updates)
1152
1140 - def _get_hf_mm_data(
1141 - self,
1142 - mm_items: MultiModalDataItems,
1143 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1144 - """Extract processor and passthrough data from multi-modal items."""
1145 - processor_data = dict[str, object]()
1146 - passthrough_data = dict[str, object]()
1147 -
1148 - for items in mm_items.values():
1149 - processor_data.update(items.get_processor_data())
1150 - passthrough_data.update(items.get_passthrough_data())
1151 -
1152 - return processor_data, passthrough_data
1153 -
1154 - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str | None:
1155 - """Get the text to pass to the HF processor alongside the multi-modal
1156 - data.
1153 + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str | None:
1154 + """Get the text to pass to the HF processor alongside the multi-modal data.
1155
1156 By default, no text is passed. If the HF processor requires that
1157 text and multi-modal items correspond to each other, you should
@@ -1162,29 +1160,53 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1160 """
1161 return None
1162
1165 - def _preprocess_hf_mm_data(
1163 + def _get_hf_mm_inputs(
1164 self,
1167 - mm_data: Mapping[str, object],
1168 - hf_processor_mm_kwargs: Mapping[str, object],
1169 - ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1170 - """Pre-process the multi-modal data and HF processor keyword arguments
1171 - before they are passed to the HF processor.
1172 -
1173 - By default, both are returned as-is. If the HF processor expects the
1174 - multi-modal data under different keys than those provided by the
1175 - multi-modal items (e.g. `audio` instead of `audios`), or requires
1176 - additional keyword arguments (e.g. `sampling_rate`), you should
1177 - override this method.
1165 + mm_items: MultiModalDataItems,
1166 + hf_kwargs: Mapping[str, object],
1167 + ) -> HFMultiModalInputs:
1168 + """Extract the multi-modal data and corresponding keyword arguments
1169 + for HF processing.
1170 +
1171 + If the HF processor expects the multi-modal data under different keys
1172 + than those provided by the multi-modal items, or requires additional
1173 + keyword arguments, you should override this method.
1174 """
1179 - return mm_data, hf_processor_mm_kwargs
1175 + hf_data = dict[str, object]()
1176 + passthrough_data = dict[str, object]()
1177 +
1178 + for items in mm_items.values():
1179 + if not items:
1180 + continue
1181 +
1182 + hf_data.update(items.get_processor_data())
1183 + passthrough_data.update(items.get_passthrough_data())
1184 +
1185 + if hf_data:
1186 + prompt_text = self._get_hf_mm_text(mm_items.get_all_counts())
1187 + if prompt_text is not None:
1188 + hf_data = dict(text=prompt_text, **hf_data)
1189 +
1190 + # vLLM needs the untruncated sequence to keep placeholder
1191 + # tokens aligned. Note that the text inputs are just dummy
1192 + # text, not the original prompt. The original prompt is
1193 + # already tokenized by the renderer.
1194 + hf_kwargs = {"truncation": False, **hf_kwargs}
1195 +
1196 + # HF processors accept (images, text, videos, audio)
1197 + # so we need to remap vLLM's "audios" -> HF "audio"
1198 + if "audios" in hf_data:
1199 + hf_data["audio"] = hf_data.pop("audios")
1200 +
1201 + return HFMultiModalInputs(hf_data, hf_kwargs, passthrough_data)
1202
1203 def _postprocess_hf_mm_data(
1204 self,
1183 - mm_data: Mapping[str, object],
1205 + mm_inputs: Mapping[str, object],
1206 hf_processor_mm_kwargs: Mapping[str, object],
1207 processed_data: BatchFeature,
1208 ) -> BatchFeature:
1187 - """Post-process the output of the HF processor.
1209 + """Post-process the combined processor and passthrough data.
1210
1211 By default, the output is returned as-is. If you need to modify the
1212 output of the HF processor before it is converted into multi-modal
@@ -1192,41 +1214,51 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1214 """
1215 return processed_data
1216
1217 + def _call_hf_processor(
1218 + self,
1219 + hf_data: Mapping[str, object],
1220 + hf_kwargs: Mapping[str, object],
1221 + ) -> BatchFeature:
1222 + """Call the HF processor on the normalized multi-modal inputs."""
1223 + return self.info.ctx.call_hf_processor(
1224 + self.info.get_hf_processor(**hf_kwargs),
1225 + hf_data,
1226 + hf_kwargs,
1227 + )
1228 +
1229 + def _finalize_hf_mm_data(
1230 + self,
1231 + hf_data: Mapping[str, object],
1232 + hf_kwargs: Mapping[str, object],
1233 + passthrough_data: Mapping[str, object],
1234 + processed_data: BatchFeature | None = None,
1235 + ) -> BatchFeature:
1236 + """Merge passthrough fields and apply model-specific postprocessing."""
1237 + if processed_data is None:
1238 + from transformers.feature_extraction_utils import BatchFeature
1239 +
1240 + processed_data = BatchFeature()
1241 +
1242 + processed_data.update(passthrough_data)
1243 + return self._postprocess_hf_mm_data(hf_data, hf_kwargs, processed_data)
1244 +
1245 def _apply_hf_processor_main(
1246 self,
1247 mm_items: MultiModalDataItems,
1198 - hf_processor_mm_kwargs: Mapping[str, object],
1248 + hf_kwargs: Mapping[str, object],
1249 ) -> BatchFeature:
1250 """Apply the HF processor on the multi-modal data."""
1201 - valid_mm_items = mm_items.select(
1202 - {k for k, c in mm_items.get_all_counts().items() if c > 0}
1251 + hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1252 + mm_items, hf_kwargs
1253 )
1204 - processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
1254
1206 - if processor_data:
1207 - processor_data, hf_processor_mm_kwargs = self._preprocess_hf_mm_data(
1208 - processor_data, hf_processor_mm_kwargs
1209 - )
1210 -
1211 - prompt_text = self._get_hf_processor_text(mm_items.get_all_counts())
1212 - if prompt_text is not None:
1213 - processor_data = dict(text=prompt_text, **processor_data)
1214 -
1215 - processed_data = self.info.ctx.call_hf_processor(
1216 - self.info.get_hf_processor(**hf_processor_mm_kwargs),
1217 - processor_data,
1218 - hf_processor_mm_kwargs,
1219 - )
1220 - processed_data.update(passthrough_data)
1255 + if hf_data:
1256 + processed_data = self._call_hf_processor(hf_data, hf_kwargs)
1257 else:
1222 - from transformers.feature_extraction_utils import BatchFeature
1223 -
1224 - processed_data = BatchFeature(dict(passthrough_data))
1258 + processed_data = None
1259
1226 - return self._postprocess_hf_mm_data(
1227 - processor_data,
1228 - hf_processor_mm_kwargs,
1229 - processed_data,
1260 + return self._finalize_hf_mm_data(
1261 + hf_data, hf_kwargs, passthrough_data, processed_data
1262 )
1263
1264 def _postprocess_prompt(self, prompt: list[int]) -> list[int]:
@@ -1261,6 +1293,9 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1293
1294 mm_missing_data = {}
1295 for modality, idxs in mm_missing_idxs.items():
1296 + if not idxs:
1297 + continue
1298 +
1299 missing_modality_data = []
1300 for idx in idxs:
1301 data = mm_data_items[modality][idx]
@@ -1271,6 +1306,7 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1306 )
1307 else:
1308 missing_modality_data.append(data)
1309 +
1310 mm_missing_data[modality] = missing_modality_data
1311
1312 mm_missing_items = self.info.parse_mm_data(mm_missing_data, validate=False)
@@ -1342,11 +1378,11 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1378 self,
1379 inputs: ProcessorInputs,
1380 timing_ctx: TimingContext,
1345 - ) -> MultiModalProcessingInfo:
1381 + ) -> MultiModalProcessingResult:
1382 with timing_ctx.record("apply_hf_processor"):
1383 mm_processed_data = self._apply_hf_processor_main(
1384 mm_items=inputs.mm_data_items,
1349 - hf_processor_mm_kwargs=inputs.hf_processor_mm_kwargs,
1385 + hf_kwargs=inputs.hf_processor_mm_kwargs,
1386 )
1387
1388 mm_kwargs = MultiModalKwargsItems.from_hf_inputs(
@@ -1369,26 +1405,28 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1405 mm_kwargs,
1406 )
1407
1372 - mm_info = MultiModalProcessingInfo(
1408 + return MultiModalProcessingResult(
1409 + prompt_ids=self._postprocess_prompt(inputs.prompt),
1410 kwargs=mm_kwargs,
1411 hashes=mm_hashes,
1412 prompt_updates=mm_prompt_updates,
1413 )
1414
1378 - return mm_info
1379 -
1415 def _cached_apply_hf_processor(
1416 self,
1417 inputs: ProcessorInputs,
1418 timing_ctx: TimingContext,
1384 - ) -> MultiModalProcessingInfo:
1419 + ) -> MultiModalProcessingResult:
1420 """Apply the HF processor on the full prompt text,
1421 caching the results and reusing cached results.
1422 """
1423 cache = self.cache
1424 + has_passthrough_data = any(
1425 + len(items.get_passthrough_data()) > 0
1426 + for items in inputs.mm_data_items.values()
1427 + )
1428
1390 - _, passthrough_data = self._get_hf_mm_data(inputs.mm_data_items)
1391 - if cache is None or passthrough_data:
1429 + if cache is None or has_passthrough_data:
1430 return self._apply_hf_processor(inputs, timing_ctx)
1431
1432 with timing_ctx.record("get_mm_hashes"):
@@ -1410,7 +1448,7 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1448 with timing_ctx.record("apply_hf_processor"):
1449 mm_missing_processed_data = self._apply_hf_processor_main(
1450 mm_items=mm_missing_data_items,
1413 - hf_processor_mm_kwargs=inputs.hf_processor_mm_kwargs,
1451 + hf_kwargs=inputs.hf_processor_mm_kwargs,
1452 )
1453
1454 mm_missing_kwargs = MultiModalKwargsItems.from_hf_inputs(
@@ -1435,14 +1473,13 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1473 mm_missing_prompt_updates=mm_missing_prompt_updates,
1474 )
1475
1438 - mm_info = MultiModalProcessingInfo(
1476 + return MultiModalProcessingResult(
1477 + prompt_ids=self._postprocess_prompt(inputs.prompt),
1478 kwargs=mm_kwargs,
1479 hashes=mm_hashes,
1480 prompt_updates=mm_prompt_updates,
1481 )
1482
1444 - return mm_info
1445 -
1483 def _apply_token_matches(
1484 self,
1485 prompt: list[int],
@@ -1670,17 +1707,15 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1707 def _maybe_apply_prompt_updates(
1708 self,
1709 mm_items: MultiModalDataItems,
1673 - prompt_ids: list[int],
1674 - mm_kwargs: MultiModalKwargsOptionalItems,
1675 - mm_prompt_updates: MultiModalPromptUpdates,
1710 + mm_res: MultiModalProcessingResult,
1711 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
1712 mm_item_counts = mm_items.get_all_counts()
1678 - self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
1679 - self._validate_mm_updates(mm_prompt_updates, mm_item_counts)
1713 + self._validate_mm_kwargs(mm_res.kwargs, mm_item_counts)
1714 + self._validate_mm_updates(mm_res.prompt_updates, mm_item_counts)
1715
1716 prompt_ids, mm_placeholders = self._apply_prompt_updates(
1682 - prompt_ids,
1683 - mm_prompt_updates,
1717 + mm_res.prompt_ids,
1718 + mm_res.prompt_updates,
1719 )
1720
1721 self._validate_mm_placeholders(mm_placeholders, mm_item_counts)
@@ -1705,15 +1740,12 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1740 4. Extract information about the placeholder tokens from the
1741 processed token IDs.
1742 """
1708 - prompt_ids = self._postprocess_prompt(inputs.prompt)
1709 - mm_info = self._cached_apply_hf_processor(inputs, timing_ctx)
1743 + mm_res = self._cached_apply_hf_processor(inputs, timing_ctx)
1744
1745 with timing_ctx.record("apply_prompt_updates"):
1746 prompt_ids, mm_placeholders = self._maybe_apply_prompt_updates(
1747 mm_items=inputs.mm_data_items,
1714 - prompt_ids=prompt_ids,
1715 - mm_kwargs=mm_info.kwargs,
1716 - mm_prompt_updates=mm_info.prompt_updates,
1748 + mm_res=mm_res,
1749 )
1750
1751 mm_placeholder_ranges = {
@@ -1723,8 +1755,8 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1755
1756 return mm_input(
1757 prompt_token_ids=prompt_ids,
1726 - mm_kwargs=mm_info.kwargs,
1727 - mm_hashes=mm_info.hashes,
1758 + mm_kwargs=mm_res.kwargs,
1759 + mm_hashes=mm_res.hashes,
1760 mm_placeholders=mm_placeholder_ranges,
1761 )
1762