[Model] Fused mm preprocess normalisation on the Device (#50411)
Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io> Signed-off-by: wang.yuqi <noooop@126.com> Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
wang.yuqi committed
Aug 6, 2026 at 18:11 UTC
2fa490470ddf0fe578f7b70cd3aa8f6c1e06b2c6
13 files changed
+359
-11
docs/design/mm_processing.md
+42
@@ -61,3 +61,45 @@ Some HF processors, such as the one for Qwen2-VL, are [very slow](https://github
61
When new data is passed in, we first check which items are in the cache, and which ones are missing. The missing items are passed into the HF processor in a single batch and cached, before being merged with the existing items in the cache.
62
63
Since we only process the missing multi-modal data items, the number of input placeholder tokens no longer corresponds to the number of the multi-modal inputs, so they can't be passed alongside the text prompt to HF processor. Therefore, we process the text and multi-modal inputs separately, using [dummy text](#dummy-text) to avoid HF errors. Since this skips HF's prompt updating code, we apply [automatic prompt updating](#automatic-prompt-updating) afterwards to keep the output tokens and multi-modal data consistent with each other.
64
+
65
+## Speeding Up Multi‑Modal Data Processing
66
+
67
+### Fused Normalisation on the Device
68
+
69
+To accelerate the multi‑modal data pipeline (decoding, resizing, normalisation, and rescaling), we offload the heavy numerical preprocessing from the CPU to the GPU and optimise data movement.
70
+
71
+#### Fusing Normalisation and Rescaling on the GPU
72
+
73
+Traditionally, the CPU would divide pixel values by 255, then subtract the mean and divide by the standard deviation. We fuse these steps into one operation and run it entirely on the GPU.
74
+
75
+- **How it works**: We use a dedicated `FusedInputNorm` module — essentially a frozen `BatchNorm1d` with the rescale factor baked into its statistics — and bake the rescale factor (typically 1/255) directly into the effective mean and standard deviation:
76
+ - Effective mean = `image_mean * (1/rescale_factor)`
77
+ - Effective std = `image_std * (1/rescale_factor)`
78
+- **At runtime**: The `FusedInputNorm` layer takes raw `uint8` pixel values (0–255) and performs the full normalised mapping in a single GPU kernel—no CPU involvement.
79
+
80
+#### Optimized Data Path for Fused Normalisation
81
+
82
+Performing fused normalisation directly on the device allows us to keep the entire transfer path—from **Entrypoint** through **Engine Core** to **GPU memory**—in **`uint8`**. This halves PCIe bandwidth and reduces CPU memory footprint.
83
+
84
+Only after data reaches GPU memory do we cast to `fp32` for the `FusedInputNorm` layer (to ensure numerical accuracy), then cast to `bf16` for subsequent layers—all within the GPU, avoiding any host‑side conversions.
85
+
86
+Overall path: **`Entrypoint (uint8) → Engine Core (uint8) → GPU Memory (uint8)`** → GPU‑local `fp32` `FusedInputNorm` → `bf16` output.
87
+
88
+#### Toggle: `mm_device_do_normalize`
89
+
90
+This GPU‑side fusion is controlled by a config flag called **`mm_device_do_normalize`**.
91
+
92
+- When `True`, normalisation and rescaling are done on the GPU using the `FusedInputNorm` layer; when `False`, we fall back to the old CPU‑side path.
93
+- The flag is **enabled by default** for all models that support it.
94
+- Currently, it’s on by default for these architectures:
95
+
96
+| name | Architecture | Example HF Models |
97
+|--------------|--------------------------------------|-------------------------------------|
98
+| `qwen2-vl` | `Qwen2VLForConditionalGeneration` | `Qwen/Qwen2-VL-2B-Instruct`, etc. |
99
+| `qwen2.5-vl` | `Qwen2_5_VLForConditionalGeneration` | `Qwen/Qwen2.5-VL-3B-Instruct`, etc. |
100
+
101
+#### What We Gain Overall
102
+
103
+- **CPU offload**: The arithmetic for normalisation and rescaling is completely gone from the CPU.
104
+- **PCIe savings**: Sending `uint8` (1 byte) instead of `bf16` (2 bytes) slashes data transfer volume by **50%** .
105
+- **GPU overhead**: The fused kernel is very lightweight and can often be merged with subsequent CUDA operations, so it hardly adds any extra cost.
tests/models/multimodal/generation_ppl_test/ppl_utils.py
+2
@@ -72,6 +72,7 @@ def vqa_ppl_test(
72
# and avoid batch different requests together.
73
74
model_config = vllm_model.llm.llm_engine.model_config
75
+ mm_device_do_normalize = model_config.multimodal_config.mm_device_do_normalize
76
77
# Confirm whether vllm is using the correct architecture
78
if model_info.architecture:
@@ -147,6 +148,7 @@ def vqa_ppl_test(
148
149
differ = (vllm_ppl - hf_ppl) / hf_ppl
150
print("Model:", model_info.name)
151
+ print("mm_device_do_normalize:", mm_device_do_normalize)
152
print("VLLM:", f"dtype:{vllm_dtype}", f"head_dtype:{head_dtype}", vllm_ppl)
153
print("Transformers:", hf_dtype, hf_ppl)
154
print("Difference (%):", differ * 100)
tests/models/multimodal/generation_ppl_test/test_qwen.py
+11
-4
@@ -8,8 +8,8 @@ from tests.models.utils import GenerateModelInfo
8
from .ppl_utils import vqa_ppl_test
9
10
MODELS = [
11
- GenerateModelInfo("Qwen/Qwen2-VL-2B-Instruct"),
12
- GenerateModelInfo("Qwen/Qwen2.5-VL-3B-Instruct"),
11
+ GenerateModelInfo("Qwen/Qwen2-VL-2B-Instruct", hf_ppl=41081356.0),
12
+ GenerateModelInfo("Qwen/Qwen2.5-VL-3B-Instruct", hf_ppl=18330016.0),
13
]
14
15
@@ -20,7 +20,14 @@ mm_processor_kwargs = {
20
21
22
@pytest.mark.parametrize("model_info", MODELS)
23
-def test_ppl(hf_runner, vllm_runner, model_info: GenerateModelInfo):
23
+@pytest.mark.parametrize("mm_device_do_normalize", [True, False])
24
+def test_ppl(
25
+ hf_runner, vllm_runner, model_info: GenerateModelInfo, mm_device_do_normalize: bool
26
+):
27
vqa_ppl_test(
25
- hf_runner, vllm_runner, model_info, mm_processor_kwargs=mm_processor_kwargs
28
+ hf_runner,
29
+ vllm_runner,
30
+ model_info,
31
+ vllm_extra_kwargs={"mm_device_do_normalize": mm_device_do_normalize},
32
+ mm_processor_kwargs=mm_processor_kwargs,
33
)
tests/models/multimodal/processing/test_qwen2_vl.py
+51
@@ -2,9 +2,11 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import pytest
5
+import torch
6
from packaging.version import Version
7
from transformers import __version__ as TRANSFORMERS_VERSION
8
9
+from vllm.model_executor.models.vision import FusedInputNorm
10
from vllm.multimodal import MULTIMODAL_REGISTRY
11
12
from ....conftest import ImageTestAssets
@@ -128,3 +130,52 @@ def test_get_image_size_with_most_features(
130
t, h, w = grid_thw[0]
131
tokens = (t * h * w) // (merge_size**2)
132
assert tokens < max_tokens
133
+
134
+
135
+@pytest.mark.parametrize(
136
+ "model_id", ["Qwen/Qwen2-VL-2B-Instruct", "Qwen/Qwen2.5-VL-3B-Instruct"]
137
+)
138
+@pytest.mark.parametrize("num_imgs", [1, 2])
139
+def test_mm_device_do_normalize(
140
+ image_assets: ImageTestAssets,
141
+ model_id: str,
142
+ num_imgs: int,
143
+):
144
+ """Ensure that enable mm_device_do_normalize yields the correct result."""
145
+
146
+ ctx = build_model_context(
147
+ model_id,
148
+ limit_mm_per_prompt={"image": num_imgs},
149
+ )
150
+ ctx.model_config.multimodal_config.mm_device_do_normalize = False
151
+ processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config)
152
+
153
+ # Build the image str / prompt based on the number of images we pass
154
+ prompt = "<|vision_start|><|image_pad|><|vision_end|>" * num_imgs
155
+ mm_data = {"image": [image_assets[0].pil_image] * num_imgs}
156
+
157
+ processed_inputs_with_normalize = processor(
158
+ prompt,
159
+ mm_items=processor.info.parse_mm_data(mm_data),
160
+ )
161
+ pixel_values_with_normalize = processed_inputs_with_normalize[
162
+ "mm_kwargs"
163
+ ].get_data()["pixel_values"]
164
+ dtype = pixel_values_with_normalize.dtype
165
+
166
+ processed_inputs_without_normalize = processor(
167
+ prompt,
168
+ mm_items=processor.info.parse_mm_data(mm_data),
169
+ hf_processor_mm_kwargs={"do_normalize": False, "do_rescale": False},
170
+ )
171
+ pixel_values_without_normalize = processed_inputs_without_normalize[
172
+ "mm_kwargs"
173
+ ].get_data()["pixel_values"]
174
+
175
+ ctx.model_config.multimodal_config.mm_device_do_normalize = True
176
+ input_norm = FusedInputNorm.from_model_config(ctx.model_config)
177
+ pixel_values_do_input_norm = input_norm(
178
+ pixel_values_without_normalize.to(dtype), dtype
179
+ )
180
+
181
+ torch.testing.assert_close(pixel_values_with_normalize, pixel_values_do_input_norm)
vllm/config/model.py
+50
@@ -396,6 +396,7 @@ class ModelConfig:
396
video_pruning_method: InitVar[str | None] = None
397
mm_tensor_ipc: InitVar[MMTensorIPC] = None
398
mm_ipc_gpu_memory_gb: InitVar[float | None] = None
399
+ mm_device_do_normalize: InitVar[bool | None] = None
400
mm_processor_device: InitVar[MMProcessorDevice | None] = None
401
402
def compute_hash(self) -> str:
@@ -527,6 +528,7 @@ class ModelConfig:
528
video_pruning_method: str | None,
529
mm_tensor_ipc: MMTensorIPC,
530
mm_ipc_gpu_memory_gb: float | None,
531
+ mm_device_do_normalize: bool | None,
532
mm_processor_device: MMProcessorDevice | None,
533
) -> None:
534
# Keep set served_model_name before maybe_model_redirect(self.model)
@@ -793,6 +795,9 @@ class ModelConfig:
795
video_pruning_method=video_pruning_method,
796
mm_tensor_ipc=mm_tensor_ipc,
797
mm_ipc_gpu_memory_gb=mm_ipc_gpu_memory_gb,
798
+ mm_device_do_normalize=self._resolve_mm_device_do_normalize(
799
+ mm_device_do_normalize
800
+ ),
801
)
802
803
mm_config_kwargs = {
@@ -927,6 +932,51 @@ class ModelConfig:
932
)
933
return self
934
935
+ def _resolve_mm_device_do_normalize(
936
+ self, mm_device_do_normalize: bool | None
937
+ ) -> bool:
938
+ if mm_device_do_normalize is None:
939
+ if envs.VLLM_USE_RUST_FRONTEND:
940
+ logger.debug(
941
+ "VLLM_USE_RUST_FRONTEND is set. "
942
+ "Rust frontend does not currently support mm_device_do_normalize, "
943
+ "forcing mm_device_do_normalize = False."
944
+ )
945
+ mm_device_do_normalize = False
946
+ else:
947
+ mm_device_do_normalize = (
948
+ self._model_info.supports_mm_device_do_normalize
949
+ )
950
+ logger.debug(
951
+ "mm_device_do_normalize is %s by default.",
952
+ "enabled" if mm_device_do_normalize else "disabled",
953
+ )
954
+ else:
955
+ if mm_device_do_normalize and envs.VLLM_USE_RUST_FRONTEND:
956
+ logger.warning(
957
+ "VLLM_USE_RUST_FRONTEND is set. "
958
+ "Rust frontend does not currently support mm_device_do_normalize, "
959
+ "forcing mm_device_do_normalize = False."
960
+ )
961
+ mm_device_do_normalize = False
962
+
963
+ if (
964
+ mm_device_do_normalize
965
+ and not self._model_info.supports_mm_device_do_normalize
966
+ ):
967
+ logger.warning(
968
+ "Model does not support mm_device_do_normalize, "
969
+ "forcing mm_device_do_normalize = False."
970
+ )
971
+ mm_device_do_normalize = False
972
+
973
+ logger.debug(
974
+ "mm_device_do_normalize is %s.",
975
+ "enabled" if mm_device_do_normalize else "disabled",
976
+ )
977
+
978
+ return mm_device_do_normalize
979
+
980
def _get_transformers_backend_cls(self) -> str:
981
"""Determine which Transformers modeling backend class will be used if
982
`model_impl` is set to `transformers` or `auto`."""
vllm/config/multimodal.py
+9
@@ -144,6 +144,11 @@ class MultiModalConfig:
144
145
For example, for Phi-3-Vision:
146
`{"num_crops": 4}`."""
147
+ mm_device_do_normalize: bool | None = True
148
+ """
149
+ Move the do_normalize computation in the mm preprocessing to before the ViT,
150
+ and let the device do it, so that CPU computation can be saved.
151
+ """
152
mm_processor_cache_gb: float = Field(default=4, ge=0)
153
"""The size (in GiB) of the multi-modal processor cache, which is used to
154
avoid re-processing past multi-modal inputs.
@@ -474,6 +479,7 @@ class MultiModalConfig:
479
self.mm_encoder_tp_mode,
480
self.mm_encoder_attn_dtype,
481
self.mm_encoder_fp8_scale_path,
482
+ self.mm_device_do_normalize,
483
]
484
hash_str = safe_hash(str(factors).encode(), usedforsecurity=False).hexdigest()
485
return hash_str
@@ -503,6 +509,9 @@ class MultiModalConfig:
509
according to the extra arguments passed during inference.
510
"""
511
kwargs = self.mm_processor_kwargs or {}
512
+ if self.mm_device_do_normalize:
513
+ kwargs["do_normalize"] = False
514
+ kwargs["do_rescale"] = False
515
return kwargs | dict(inference_kwargs)
516
517
def use_gpu_video_backend(self) -> bool:
vllm/engine/arg_utils.py
+9
@@ -599,6 +599,7 @@ class EngineArgs:
599
mm_tensor_ipc: MMTensorIPC = MultiModalConfig.mm_tensor_ipc
600
mm_processor_device: MMProcessorDevice = "auto"
601
mm_ipc_gpu_memory_gb: float = MultiModalConfig.mm_ipc_gpu_memory_gb
602
+ mm_device_do_normalize: bool | None = MultiModalConfig.mm_device_do_normalize
603
# LoRA fields
604
enable_lora: bool = False
605
max_loras: int = LoRAConfig.max_loras
@@ -1383,6 +1384,13 @@ class EngineArgs:
1384
"--mm-ipc-gpu-memory-gb",
1385
**multimodal_kwargs["mm_ipc_gpu_memory_gb"],
1386
)
1387
+ multimodal_group.add_argument(
1388
+ "--mm-device-do-normalize",
1389
+ **{
1390
+ **multimodal_kwargs["mm_device_do_normalize"],
1391
+ "default": None,
1392
+ },
1393
+ )
1394
1395
# LoRA related configs
1396
lora_kwargs = get_kwargs(LoRAConfig)
@@ -1769,6 +1777,7 @@ class EngineArgs:
1777
video_pruning_method=self.video_pruning_method,
1778
mm_tensor_ipc=self.mm_tensor_ipc,
1779
mm_ipc_gpu_memory_gb=self.mm_ipc_gpu_memory_gb,
1780
+ mm_device_do_normalize=self.mm_device_do_normalize,
1781
mm_processor_device=self.mm_processor_device,
1782
io_processor_plugin=self.io_processor_plugin,
1783
renderer_num_workers=self.renderer_num_workers,
vllm/model_executor/models/interfaces.py
+6
@@ -149,6 +149,12 @@ class SupportsMultiModal(SupportsMultiModalEmbeddings, Protocol):
149
`multimodal_config.mm_encoder_tp_mode="data"`.
150
"""
151
152
+ supports_mm_device_do_normalize: ClassVar[bool] = False
153
+ """
154
+ A flag that indicates whether this model supports
155
+ `multimodal_config.mm_device_do_normalize`.
156
+ """
157
+
158
requires_raw_input_tokens: ClassVar[bool] = False
159
"""
160
A flag that indicates this model processes input id tokens
vllm/model_executor/models/qwen2_5_vl.py
+9
@@ -112,6 +112,7 @@ from .utils import (
112
maybe_prefix,
113
)
114
from .vision import (
115
+ FusedInputNorm,
116
get_fp8_padded_hidden_size,
117
get_vit_attn_backend,
118
is_vit_use_data_parallel,
@@ -1254,6 +1255,7 @@ class Qwen2_5_VLForConditionalGeneration(
1255
)
1256
1257
supports_encoder_tp_data = True
1258
+ supports_mm_device_do_normalize = True
1259
1260
def iter_mm_grid_thw(
1261
self, mm_features: list[MultiModalFeatureSpec]
@@ -1363,6 +1365,7 @@ class Qwen2_5_VLForConditionalGeneration(
1365
quant_config=self.quant_config,
1366
prefix=maybe_prefix(prefix, "visual"),
1367
)
1368
+ self.input_norm = FusedInputNorm.from_model_config(self.model_config)
1369
1370
with self._mark_language_model(vllm_config):
1371
self.language_model = init_vllm_registered_model(
@@ -1437,6 +1440,8 @@ class Qwen2_5_VLForConditionalGeneration(
1440
image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1441
else:
1442
pixel_values = image_input["pixel_values"]
1443
+ pixel_values = self.input_norm(pixel_values, self.visual.dtype)
1444
+
1445
if self.use_data_parallel:
1446
return run_dp_sharded_mrope_vision_model(
1447
self.visual, pixel_values, grid_thw_list, rope_type="rope_3d"
@@ -1493,6 +1498,10 @@ class Qwen2_5_VLForConditionalGeneration(
1498
video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1499
else:
1500
pixel_values_videos = video_input["pixel_values_videos"]
1501
+ pixel_values_videos = self.input_norm(
1502
+ pixel_values_videos, self.visual.dtype
1503
+ )
1504
+
1505
if self.use_data_parallel:
1506
return run_dp_sharded_mrope_vision_model(
1507
self.visual,
vllm/model_executor/models/qwen2_vl.py
+9
-2
@@ -103,6 +103,7 @@ from .utils import (
103
maybe_prefix,
104
)
105
from .vision import (
106
+ FusedInputNorm,
107
get_vit_attn_backend,
108
is_vit_use_data_parallel,
109
run_dp_sharded_mrope_vision_model,
@@ -1192,6 +1193,7 @@ class Qwen2VLForConditionalGeneration(
1193
)
1194
1195
supports_encoder_tp_data = True
1196
+ supports_mm_device_do_normalize = True
1197
1198
def iter_mm_grid_thw(
1199
self, mm_features: list[MultiModalFeatureSpec]
@@ -1296,6 +1298,7 @@ class Qwen2VLForConditionalGeneration(
1298
quant_config=quant_config,
1299
prefix=maybe_prefix(prefix, "visual"),
1300
)
1301
+ self.input_norm = FusedInputNorm.from_model_config(self.model_config)
1302
1303
with self._mark_language_model(vllm_config):
1304
self.language_model = init_vllm_registered_model(
@@ -1363,9 +1366,10 @@ class Qwen2VLForConditionalGeneration(
1366
assert grid_thw.ndim == 2
1367
1368
if image_input["type"] == "image_embeds":
1366
- image_embeds = image_input["image_embeds"]
1369
+ image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1370
else:
1371
pixel_values = image_input["pixel_values"]
1372
+ pixel_values = self.input_norm(pixel_values, self.visual.dtype)
1373
1374
if self.use_data_parallel:
1375
return run_dp_sharded_mrope_vision_model(
@@ -1386,9 +1390,12 @@ class Qwen2VLForConditionalGeneration(
1390
assert grid_thw.ndim == 2
1391
1392
if video_input["type"] == "video_embeds":
1389
- video_embeds = video_input["video_embeds"]
1393
+ video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1394
else:
1395
pixel_values_videos = video_input["pixel_values_videos"]
1396
+ pixel_values_videos = self.input_norm(
1397
+ pixel_values_videos, self.visual.dtype
1398
+ )
1399
if self.use_data_parallel:
1400
return run_dp_sharded_mrope_vision_model(
1401
self.visual,
vllm/model_executor/models/registry.py
+4
@@ -814,6 +814,7 @@ class _ModelInfo:
814
supports_transcription: bool
815
supports_transcription_only: bool
816
supported_video_pruning_methods: tuple[str, ...]
817
+ supports_mm_device_do_normalize: bool
818
819
@staticmethod
820
def from_model_cls(model: type[nn.Module]) -> "_ModelInfo":
@@ -847,6 +848,9 @@ class _ModelInfo:
848
supported_video_pruning_methods=getattr(
849
model, "supported_video_pruning_methods", ()
850
),
851
+ supports_mm_device_do_normalize=getattr(
852
+ model, "supports_mm_device_do_normalize", False
853
+ ),
854
)
855
856
vllm/model_executor/models/vision.py
+141
-1
@@ -8,9 +8,11 @@ from collections.abc import Callable
8
from typing import Final, Generic, Literal, Protocol, TypeAlias, TypeVar
9
10
import torch
11
+import torch.nn as nn
12
+import torch.nn.functional as F
13
from transformers import PretrainedConfig
14
13
-from vllm.config import MultiModalConfig, get_current_vllm_config_or_none
15
+from vllm.config import ModelConfig, MultiModalConfig, get_current_vllm_config_or_none
16
from vllm.distributed import (
17
get_tensor_model_parallel_rank,
18
get_tensor_model_parallel_world_size,
@@ -18,6 +20,7 @@ from vllm.distributed import (
20
)
21
from vllm.logger import init_logger
22
from vllm.platforms import current_platform
23
+from vllm.transformers_utils.processor import get_processor, get_processor_config
24
from vllm.utils.math_utils import round_up
25
from vllm.v1.attention.backends.registry import AttentionBackendEnum
26
@@ -580,3 +583,140 @@ def run_dp_sharded_mrope_vision_model(
583
"Found unassigned embeddings"
584
)
585
return out_embeddings
586
+
587
+
588
+class FusedInputNorm(nn.Module):
589
+ """
590
+ Module that applies rescaling and normalization to input images.
591
+ Equivalent to: output = (input * rescale_factor - mean) / std
592
+ """
593
+
594
+ def __init__(
595
+ self,
596
+ image_mean: list[float],
597
+ image_std: list[float],
598
+ rescale_factor: float,
599
+ channel: int = 3,
600
+ dtype: torch.dtype = torch.float32,
601
+ ):
602
+ super().__init__()
603
+
604
+ self.channel = channel
605
+
606
+ image_mean_tensor = torch.tensor(image_mean, dtype=dtype) * (
607
+ 1.0 / rescale_factor
608
+ )
609
+ image_std_tensor = torch.tensor(image_std, dtype=dtype) * (1.0 / rescale_factor)
610
+ weight = 1.0 / image_std_tensor
611
+ bias = -image_mean_tensor / image_std_tensor
612
+
613
+ self.is_identity = bool(
614
+ torch.allclose(weight, torch.ones_like(weight))
615
+ and torch.allclose(bias, torch.zeros_like(bias))
616
+ )
617
+
618
+ if not self.is_identity:
619
+ self.register_buffer("weight", weight)
620
+ self.register_buffer("bias", bias)
621
+ self.register_buffer("running_mean", torch.zeros_like(image_mean_tensor))
622
+ self.register_buffer("running_var", torch.ones_like(image_mean_tensor))
623
+ else:
624
+ self.register_buffer("weight", None)
625
+ self.register_buffer("bias", None)
626
+ self.register_buffer("running_mean", None)
627
+ self.register_buffer("running_var", None)
628
+
629
+ @property
630
+ def dtype(self) -> torch.dtype:
631
+ return self.weight.dtype
632
+
633
+ @classmethod
634
+ def identity(
635
+ cls, channel: int = 3, dtype: torch.dtype = torch.float32
636
+ ) -> "FusedInputNorm":
637
+ return cls(
638
+ image_mean=[0.0, 0.0, 0.0],
639
+ image_std=[1.0, 1.0, 1.0],
640
+ rescale_factor=1.0,
641
+ channel=channel,
642
+ dtype=dtype,
643
+ )
644
+
645
+ @classmethod
646
+ def from_model_config(cls, model_config: "ModelConfig") -> nn.Module:
647
+ if not model_config.multimodal_config.mm_device_do_normalize:
648
+ return cls.identity()
649
+
650
+ model = model_config.model
651
+ revision = model_config.revision
652
+
653
+ # Try to read parameters from the processor config
654
+ config = get_processor_config(model, revision=revision)
655
+ do_rescale = config.get("do_rescale", None)
656
+ do_normalize = config.get("do_normalize", None)
657
+ image_mean = config.get("image_mean", None)
658
+ image_std = config.get("image_std", None)
659
+ rescale_factor = config.get("rescale_factor", None)
660
+
661
+ # Fallback to the image_processor object if any parameter is missing
662
+ if None in [do_rescale, do_normalize, image_mean, image_std, rescale_factor]:
663
+ image_processor = get_processor(model, revision=revision).image_processor
664
+
665
+ if do_rescale is None:
666
+ do_rescale = getattr(image_processor, "do_rescale", None)
667
+ if do_normalize is None:
668
+ do_normalize = getattr(image_processor, "do_normalize", None)
669
+ if image_mean is None:
670
+ image_mean = getattr(image_processor, "image_mean", None)
671
+ if image_std is None:
672
+ image_std = getattr(image_processor, "image_std", None)
673
+ if rescale_factor is None:
674
+ rescale_factor = getattr(image_processor, "rescale_factor", None)
675
+
676
+ # Apply defaults based on flags
677
+ if not do_rescale:
678
+ rescale_factor = 1.0
679
+ if not do_normalize:
680
+ image_mean = [0.0, 0.0, 0.0]
681
+ image_std = [1.0, 1.0, 1.0]
682
+
683
+ # Ensure all required parameters are resolved
684
+ assert None not in [
685
+ do_rescale,
686
+ do_normalize,
687
+ image_mean,
688
+ image_std,
689
+ rescale_factor,
690
+ ], "Some normalization parameters are still None after resolution."
691
+
692
+ # If no processing is needed, return an identity module
693
+ if not do_rescale and not do_normalize:
694
+ return cls.identity()
695
+
696
+ return cls(
697
+ image_mean=image_mean, image_std=image_std, rescale_factor=rescale_factor
698
+ )
699
+
700
+ def forward(
701
+ self,
702
+ grid_thw: torch.Tensor,
703
+ visual_dtype: torch.dtype,
704
+ ) -> torch.Tensor:
705
+ if self.is_identity:
706
+ return grid_thw.to(visual_dtype)
707
+
708
+ assert grid_thw.ndim == 2
709
+ patches, size = grid_thw.shape
710
+ patch_size = size // self.channel
711
+
712
+ grid_thw = grid_thw.view(patches, self.channel, patch_size)
713
+ grid_thw = F.batch_norm(
714
+ grid_thw.to(self.dtype),
715
+ running_mean=self.running_mean,
716
+ running_var=self.running_var,
717
+ weight=self.weight,
718
+ bias=self.bias,
719
+ training=False,
720
+ eps=0.0,
721
+ )
722
+ return grid_thw.view(patches, size).to(visual_dtype)
vllm/transformers_utils/processor.py
+16
-4
@@ -142,10 +142,10 @@ def _merge_mm_kwargs(
142
return allowed_kwargs
143
144
145
-def get_processor_cls_name_from_config(
145
+def get_processor_config(
146
processor_name: str,
147
revision: str | None = "main",
148
-) -> str | None:
148
+) -> dict:
149
config_file = [
150
"processor_config.json",
151
"preprocessor_config.json",
@@ -154,8 +154,20 @@ def get_processor_cls_name_from_config(
154
for file in config_file:
155
config = get_hf_file_to_dict(file, processor_name, revision=revision)
156
if config and "processor_class" in config:
157
- return config["processor_class"]
158
- return None
157
+ return config
158
+ return {}
159
+
160
+
161
+def get_processor_cls_name_from_config(
162
+ processor_name: str,
163
+ revision: str | None = "main",
164
+) -> str | None:
165
+ config = get_processor_config(processor_name, revision=revision)
166
+
167
+ if not config:
168
+ return None
169
+
170
+ return config["processor_class"]
171
172
173
def get_video_processor_cls_name_from_config(