[Multimodal] Expose mm hash algothrim selection to cli args (#49686)

Signed-off-by: Isotr0py <Isotr0py@outlook.com>

Isotr0py committed Jul 31, 2026 at 13:25 UTC 1180b604f8d5559a00d17b8f384ddcdbc9a41d16
17 files changed +234 -61
docs/usage/security.md
+8 -2
@@ -396,7 +396,7 @@ FIPS compliance depends on many factors, so a vLLM deployment is not automatical
396
397 Operators running vLLM on FIPS-enabled hosts should select FIPS-approved algorithms via the following knobs:
398
399 -- **Multimodal input hashing** — `VLLM_MM_HASHER_ALGORITHM` defaults to `blake3`, which is not FIPS-approved. Set it to `sha256` or `sha512` in FIPS-enabled environments.
399 +- **Multimodal input hashing** — `--mm-hasher-algorithm` (config field `mm_hasher_algorithm`) defaults to `blake3`, which is not FIPS-approved. Set it to `sha256` or `sha512` in FIPS-enabled environments.
400 - **Prefix-cache hashing** — set `--prefix-caching-hash-algo` (config field `prefix_caching_hash_algo`) to `sha256` or `sha256_cbor`. The `xxhash` and `xxhash_cbor` options are not FIPS-approved.
401 - **TLS ciphers** — use `--ssl-ciphers` to restrict the API server's TLS handshake to FIPS-approved cipher suites that match your environment's policy.
402
@@ -408,7 +408,13 @@ vLLM uses MD5 in a few places to derive non-security cache keys (for example, co
408
409 Some dependencies expose hash implementations that are not FIPS-approved. vLLM only invokes them when the corresponding algorithm is selected, but operators with strict cryptographic controls may want to ensure the code paths are not exercised — and, where policy requires, that the packages themselves are absent:
410
411 -- `blake3` — currently listed in `requirements/common.txt`, so a standard install pulls it in. It is imported lazily and only used when `VLLM_MM_HASHER_ALGORITHM=blake3` (the default). Setting `VLLM_MM_HASHER_ALGORITHM` to `sha256` or `sha512` is sufficient to keep the non-FIPS code path dormant. If your policy additionally forbids the package being present, uninstall it after `pip install` (`pip uninstall blake3`); vLLM will continue to function as long as `VLLM_MM_HASHER_ALGORITHM` is set to a non-blake3 value.
411 +- `blake3` — currently listed in `requirements/common.txt`, so a standard
412 + install pulls it in. It is imported lazily and only used when
413 + `mm_hasher_algorithm=blake3` (the default). Setting
414 + `--mm-hasher-algorithm sha256` or `--mm-hasher-algorithm sha512` is sufficient
415 + to keep the non-FIPS code path dormant. If your policy additionally forbids
416 + the package being present, uninstall it after installation; vLLM will
417 + continue to function as long as a non-blake3 algorithm is selected.
418 - `xxhash` — a true optional dependency (not in `requirements/common.txt`). It is only imported when an `xxhash`-based prefix-cache algorithm is selected. Leave it uninstalled and select a `sha256`-based prefix-cache algorithm.
419
420 ### Beyond hashing: other FIPS considerations
tests/config/test_multimodal_config.py
+5
@@ -24,6 +24,11 @@ def test_mm_encoder_attn_backend_invalid():
24 MultiModalConfig(mm_encoder_attn_backend="not_a_backend") # type: ignore[arg-type]
25
26
27 +def test_mm_hasher_algorithm_invalid():
28 + with pytest.raises(ValueError, match="mm_hasher_algorithm"):
29 + MultiModalConfig(mm_hasher_algorithm="md5") # type: ignore[arg-type]
30 +
31 +
32 def test_mm_encoder_attn_backend_hash_updates():
33 base_hash = MultiModalConfig().compute_hash()
34 overridden_hash = MultiModalConfig(
tests/models/multimodal/processing/test_moss_audio.py
+1
@@ -50,6 +50,7 @@ class _Tokenizer:
50 class _MMConfig:
51 enable_mm_embeds = False
52 mm_processor_cache_gb = 1
53 + mm_hasher_algorithm = "blake3"
54
55 def merge_mm_processor_kwargs(self, kwargs):
56 return dict(kwargs)
tests/multimodal/test_cache.py
+2 -1
@@ -144,7 +144,8 @@ def _compare_caches(
144 for _ in range(int(item_capacity / hit_rate))
145 ]
146 all_hashes = [
147 - MultiModalHasher.hash_kwargs(item=item.get_data()) for item in all_items
147 + MultiModalHasher.hash_kwargs("blake3", item=item.get_data())
148 + for item in all_items
149 ]
150
151 prompt_update = PromptInsertion("dummy", "target", "insertion").resolve(0)
tests/multimodal/test_hasher.py
+58 -16
@@ -1,5 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import hashlib
4 import uuid
5 from io import BytesIO
6 from pathlib import Path
@@ -9,6 +10,7 @@ import pytest
10 import torch
11 from PIL import Image, ImageDraw
12
13 +from vllm.config.multimodal import MMHasherAlgorithm
14 from vllm.multimodal.hasher import MultiModalHasher
15 from vllm.multimodal.media.base import MediaWithBytes
16 from vllm.multimodal.media.image import ImageMediaIO
@@ -20,12 +22,26 @@ ASSETS_DIR = Path(__file__).parent / "assets"
22 assert ASSETS_DIR.exists()
23
24
25 +@pytest.mark.parametrize("algorithm", ["sha256", "sha512"])
26 +def test_hash_algorithm(algorithm: MMHasherAlgorithm):
27 + hasher = getattr(hashlib, algorithm)()
28 + for bytes_ in MultiModalHasher.iter_item_to_bytes("value", "test"):
29 + hasher.update(bytes_)
30 +
31 + assert MultiModalHasher.hash_kwargs(algorithm, value="test") == hasher.hexdigest()
32 +
33 +
34 +def test_hash_algorithm_required():
35 + with pytest.raises(TypeError, match="algorithm"):
36 + MultiModalHasher.hash_kwargs(value="test") # type: ignore[call-arg]
37 +
38 +
39 def test_hash_single_item_different_shape():
40 x1 = torch.zeros(())
41 x2 = torch.zeros((1,))
42
43 hasher = MultiModalHasher
28 - assert hasher.hash_kwargs(x=x1) != hasher.hash_kwargs(x=x2)
44 + assert hasher.hash_kwargs("blake3", x=x1) != hasher.hash_kwargs("blake3", x=x2)
45
46
47 def test_hash_key_order_invariant():
@@ -33,7 +49,9 @@ def test_hash_key_order_invariant():
49 y = torch.ones((5, 10))
50
51 hasher = MultiModalHasher
36 - assert hasher.hash_kwargs(x=x, y=y) == hasher.hash_kwargs(y=y, x=x)
52 + assert hasher.hash_kwargs("blake3", x=x, y=y) == hasher.hash_kwargs(
53 + "blake3", y=y, x=x
54 + )
55
56
57 # NOTE: Images that are the same visually are allowed to have the same hash
@@ -44,7 +62,9 @@ def test_hash_collision_image_mode(mode_pair):
62 image2 = Image.new(mode2, size=(10, 10), color=1)
63
64 hasher = MultiModalHasher
47 - assert hasher.hash_kwargs(image=image1) != hasher.hash_kwargs(image=image2)
65 + assert hasher.hash_kwargs("blake3", image=image1) != hasher.hash_kwargs(
66 + "blake3", image=image2
67 + )
68
69
70 def test_hash_collision_image_palette():
@@ -53,7 +73,9 @@ def test_hash_collision_image_palette():
73 image2 = Image.open(ASSETS_DIR / "image2.png")
74
75 hasher = MultiModalHasher
56 - assert hasher.hash_kwargs(image=image1) != hasher.hash_kwargs(image=image2)
76 + assert hasher.hash_kwargs("blake3", image=image1) != hasher.hash_kwargs(
77 + "blake3", image=image2
78 + )
79
80
81 def test_hash_collision_image_transpose():
@@ -64,7 +86,9 @@ def test_hash_collision_image_transpose():
86 ImageDraw.Draw(image2).line([(0, 0), (0, 10)])
87
88 hasher = MultiModalHasher
67 - assert hasher.hash_kwargs(image=image1) != hasher.hash_kwargs(image=image2)
89 + assert hasher.hash_kwargs("blake3", image=image1) != hasher.hash_kwargs(
90 + "blake3", image=image2
91 + )
92
93
94 @pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
@@ -74,7 +98,9 @@ def test_hash_collision_tensor_shape(dtype):
98 arr2 = torch.zeros((10, 20, 5, 3), dtype=dtype)
99
100 hasher = MultiModalHasher
77 - assert hasher.hash_kwargs(data=arr1) != hasher.hash_kwargs(data=arr2)
101 + assert hasher.hash_kwargs("blake3", data=arr1) != hasher.hash_kwargs(
102 + "blake3", data=arr2
103 + )
104
105
106 def test_hash_collision_array_shape():
@@ -83,7 +109,9 @@ def test_hash_collision_array_shape():
109 arr2 = np.zeros((10, 20, 5, 3))
110
111 hasher = MultiModalHasher
86 - assert hasher.hash_kwargs(data=arr1) != hasher.hash_kwargs(data=arr2)
112 + assert hasher.hash_kwargs("blake3", data=arr1) != hasher.hash_kwargs(
113 + "blake3", data=arr2
114 + )
115
116
117 def test_hash_collision_video_num_frames():
@@ -104,8 +132,8 @@ def test_hash_collision_video_num_frames():
132 return items.get_all_items_for_hash()[0]
133
134 hasher = MultiModalHasher
107 - assert hasher.hash_kwargs(video=item_for_hash(2)) != hasher.hash_kwargs(
108 - video=item_for_hash(4)
135 + assert hasher.hash_kwargs("blake3", video=item_for_hash(2)) != hasher.hash_kwargs(
136 + "blake3", video=item_for_hash(4)
137 )
138
139
@@ -118,7 +146,9 @@ def test_hash_non_contiguous_array():
146
147 hasher = MultiModalHasher
148 # Both should be hashable and produce the same hashes
121 - assert hasher.hash_kwargs(data=arr) == hasher.hash_kwargs(data=arr_c)
149 + assert hasher.hash_kwargs("blake3", data=arr) == hasher.hash_kwargs(
150 + "blake3", data=arr_c
151 + )
152
153
154 def test_hash_image_exif_id():
@@ -133,9 +163,13 @@ def test_hash_image_exif_id():
163
164 hasher = MultiModalHasher
165 # first image has UUID in ImageID, so it should hash to that UUID
136 - assert hasher.hash_kwargs(image=image1) == hasher.hash_kwargs(image=id.bytes)
166 + assert hasher.hash_kwargs("blake3", image=image1) == hasher.hash_kwargs(
167 + "blake3", image=id.bytes
168 + )
169 # second image has non-UUID in ImageID, so it should hash to the image data
138 - assert hasher.hash_kwargs(image=image2) == hasher.hash_kwargs(image=image2a)
170 + assert hasher.hash_kwargs("blake3", image=image2) == hasher.hash_kwargs(
171 + "blake3", image=image2a
172 + )
173
174
175 def _rgba_png_bytes() -> bytes:
@@ -153,9 +187,15 @@ def test_hash_collision_media_io_config():
187 keep = ImageMediaIO(image_mode=None).load_bytes(data)
188
189 hasher = MultiModalHasher
156 - assert hasher.hash_kwargs(image=white) != hasher.hash_kwargs(image=black)
157 - assert hasher.hash_kwargs(image=white) != hasher.hash_kwargs(image=keep)
158 - assert hasher.hash_kwargs(image=white) == hasher.hash_kwargs(image=white2)
190 + assert hasher.hash_kwargs("blake3", image=white) != hasher.hash_kwargs(
191 + "blake3", image=black
192 + )
193 + assert hasher.hash_kwargs("blake3", image=white) != hasher.hash_kwargs(
194 + "blake3", image=keep
195 + )
196 + assert hasher.hash_kwargs("blake3", image=white) == hasher.hash_kwargs(
197 + "blake3", image=white2
198 + )
199
200
201 def test_hash_media_io_noop_config_preserves_hash():
@@ -169,4 +209,6 @@ def test_hash_media_io_noop_config_preserves_hash():
209
210 plain = MediaWithBytes(loaded.media, data)
211 hasher = MultiModalHasher
172 - assert hasher.hash_kwargs(image=loaded) == hasher.hash_kwargs(image=plain)
212 + assert hasher.hash_kwargs("blake3", image=loaded) == hasher.hash_kwargs(
213 + "blake3", image=plain
214 + )
tests/multimodal/test_utils.py
+25
@@ -199,6 +199,31 @@ def test_group_and_batch_mm_items_split_by_fieldset():
199 assert [num_items for num_items, _ in res] == [2, 1, 1, 1]
200
201
202 +def test_group_and_batch_mm_items_splits_shared_data_by_dtype():
203 + elem1 = MultiModalFieldElem(
204 + data=torch.zeros(1, dtype=torch.int32),
205 + field=MultiModalSharedField(batch_size=1),
206 + )
207 + elem2 = MultiModalFieldElem(
208 + data=torch.zeros(1, dtype=torch.float32),
209 + field=MultiModalSharedField(batch_size=1),
210 + )
211 + elem3 = MultiModalFieldElem(
212 + data=[torch.zeros(1, dtype=torch.int32), torch.zeros(1, dtype=torch.float32)],
213 + field=MultiModalSharedField(batch_size=1),
214 + )
215 +
216 + res = group_and_batch_mm_items(
217 + [
218 + MultiModalKwargsItem({"x": elem1}),
219 + MultiModalKwargsItem({"x": elem2}),
220 + MultiModalKwargsItem({"x": elem3}),
221 + ]
222 + )
223 +
224 + assert [num_items for num_items, _ in res] == [1, 1, 1]
225 +
226 +
227 def test_group_and_batch_mm_items_split_by_shared_data():
228 elem1 = MultiModalFieldElem(
229 data=torch.zeros(1, dtype=torch.uint8),
vllm/config/model.py
+4
@@ -17,6 +17,7 @@ from vllm.config.model_arch import (
17 from vllm.config.multimodal import (
18 MMCacheType,
19 MMEncoderTPMode,
20 + MMHasherAlgorithm,
21 MMTensorIPC,
22 MultiModalConfig,
23 )
@@ -374,6 +375,7 @@ class ModelConfig:
375 mm_processor_kwargs: InitVar[dict[str, Any] | None] = None
376 mm_processor_cache_gb: InitVar[float | None] = None
377 mm_processor_cache_type: InitVar[MMCacheType | None] = None
378 + mm_hasher_algorithm: InitVar[MMHasherAlgorithm | None] = None
379 mm_shm_cache_max_object_size_mb: InitVar[int | None] = None
380 mm_encoder_only: InitVar[bool | None] = None
381 mm_encoder_tp_mode: InitVar[MMEncoderTPMode | None] = None
@@ -503,6 +505,7 @@ class ModelConfig:
505 mm_processor_kwargs: dict[str, Any] | None,
506 mm_processor_cache_gb: float | None,
507 mm_processor_cache_type: MMCacheType | None,
508 + mm_hasher_algorithm: MMHasherAlgorithm | None,
509 mm_shm_cache_max_object_size_mb: int | None,
510 mm_encoder_only: bool | None,
511 mm_encoder_tp_mode: MMEncoderTPMode | None,
@@ -737,6 +740,7 @@ class ModelConfig:
740 mm_processor_kwargs=mm_processor_kwargs,
741 mm_processor_cache_gb=mm_processor_cache_gb,
742 mm_processor_cache_type=mm_processor_cache_type,
743 + mm_hasher_algorithm=mm_hasher_algorithm,
744 mm_shm_cache_max_object_size_mb=mm_shm_cache_max_object_size_mb,
745 mm_encoder_only=mm_encoder_only,
746 mm_encoder_tp_mode=mm_encoder_tp_mode,
vllm/config/multimodal.py
+20 -2
@@ -3,13 +3,13 @@
3
4 from collections.abc import Mapping
5 from pathlib import Path
6 -from typing import Any, Literal, TypeAlias, TypedDict, final
6 +from typing import Any, Literal, TypeAlias, TypedDict, cast, final
7
8 from pydantic import ConfigDict, Field, field_validator, model_validator
9 from pydantic.dataclasses import dataclass
10
11 import vllm.envs as envs
12 -from vllm.config.utils import config
12 +from vllm.config.utils import config, get_from_deprecated_env_if_set
13 from vllm.utils.hashing import safe_hash
14 from vllm.v1.attention.backends.registry import AttentionBackendEnum
15
@@ -63,6 +63,19 @@ MMEncoderTPMode = Literal["weights", "data"]
63 MMCacheType = Literal["shm", "lru"]
64 VideoPruningMethod = Literal["evs", "vidcom2"]
65 MMTensorIPC = Literal["direct_rpc", "torch_shm"]
66 +MMHasherAlgorithm = Literal["blake3", "sha256", "sha512"]
67 +
68 +
69 +def _get_mm_hasher_algorithm() -> MMHasherAlgorithm:
70 + env_value = get_from_deprecated_env_if_set(
71 + "VLLM_MM_HASHER_ALGORITHM",
72 + "v0.27",
73 + "mm_hasher_algorithm",
74 + )
75 + env_value = "blake3" if env_value is None else env_value
76 + return cast(MMHasherAlgorithm, env_value.lower())
77 +
78 +
79 MMDummyOptions: TypeAlias = dict[str, BaseDummyOptions]
80 """
81 A dictionary containing an entry for each modality type of dummy data.
@@ -134,6 +147,11 @@ class MultiModalConfig:
147 mm_processor_cache_type: MMCacheType = "lru"
148 """Type of cache to use for the multi-modal preprocessor/mapper. If `shm`,
149 use shared memory FIFO cache. If `lru`, use mirrored LRU cache."""
150 + mm_hasher_algorithm: MMHasherAlgorithm = Field(
151 + default_factory=_get_mm_hasher_algorithm
152 + )
153 + """Hash algorithm to use for multi-modal input caching. Use `"sha256"` or
154 + `"sha512"` for FIPS-compliant deployments."""
155 mm_shm_cache_max_object_size_mb: int = Field(default=128, ge=0)
156 """Size limit (in MiB) for each object stored in the multi-modal processor
157 shared memory cache. Only effective when `mm_processor_cache_type` is
vllm/engine/arg_utils.py
+13 -1
@@ -86,7 +86,12 @@ from vllm.config.model import (
86 RunnerOption,
87 TokenizerMode,
88 )
89 -from vllm.config.multimodal import MMCacheType, MMEncoderTPMode, MMTensorIPC
89 +from vllm.config.multimodal import (
90 + MMCacheType,
91 + MMEncoderTPMode,
92 + MMHasherAlgorithm,
93 + MMTensorIPC,
94 +)
95 from vllm.config.observability import DetailedTraceModules
96 from vllm.config.parallel import (
97 All2AllBackend,
@@ -566,6 +571,9 @@ class EngineArgs:
571 mm_processor_cache_type: MMCacheType | None = (
572 MultiModalConfig.mm_processor_cache_type
573 )
574 + mm_hasher_algorithm: MMHasherAlgorithm = get_field(
575 + MultiModalConfig, "mm_hasher_algorithm"
576 + )
577 mm_shm_cache_max_object_size_mb: int = (
578 MultiModalConfig.mm_shm_cache_max_object_size_mb
579 )
@@ -1295,6 +1303,9 @@ class EngineArgs:
1303 multimodal_group.add_argument(
1304 "--mm-processor-cache-type", **multimodal_kwargs["mm_processor_cache_type"]
1305 )
1306 + multimodal_group.add_argument(
1307 + "--mm-hasher-algorithm", **multimodal_kwargs["mm_hasher_algorithm"]
1308 + )
1309 multimodal_group.add_argument(
1310 "--mm-shm-cache-max-object-size-mb",
1311 **multimodal_kwargs["mm_shm_cache_max_object_size_mb"],
@@ -1712,6 +1723,7 @@ class EngineArgs:
1723 mm_processor_cache_gb=self.mm_processor_cache_gb,
1724 mm_processor_cache_type=self.mm_processor_cache_type,
1725 mm_shm_cache_max_object_size_mb=self.mm_shm_cache_max_object_size_mb,
1726 + mm_hasher_algorithm=self.mm_hasher_algorithm,
1727 mm_encoder_only=self.mm_encoder_only,
1728 mm_encoder_tp_mode=self.mm_encoder_tp_mode,
1729 mm_encoder_attn_backend=self.mm_encoder_attn_backend,
vllm/model_executor/models/terratorch.py
+4 -1
@@ -217,7 +217,10 @@ class TerratorchMultiModalProcessor(BaseMultiModalProcessor[TerratorchProcessing
217 )
218
219 with timing_ctx.record("get_mm_hashes"):
220 - mm_hashes = inputs.get_mm_hashes(self.info.model_id)
220 + mm_hashes = inputs.get_mm_hashes(
221 + self.info.model_id,
222 + self.info.ctx.get_mm_config().mm_hasher_algorithm,
223 + )
224
225 mm_placeholders = {"image": [PlaceholderRange(offset=0, length=0)]}
226
vllm/model_executor/models/transformers/multimodal.py
+4 -1
@@ -402,7 +402,10 @@ class MultiModalProcessor(BaseMultiModalProcessor[MultiModalProcessingInfo]):
402
403 # Use overrides if provided; fallback to data-dependent hashing.
404 with timing_ctx.record("get_mm_hashes"):
405 - mm_hashes = inputs.get_mm_hashes(self.info.model_id)
405 + mm_hashes = inputs.get_mm_hashes(
406 + self.info.model_id,
407 + self.info.ctx.get_mm_config().mm_hasher_algorithm,
408 + )
409
410 # For gemma3 we check `token_type_ids` as the key
411 mm_token_type_ids = processed_data.pop("token_type_ids", None)
vllm/multimodal/hasher.py
+12 -6
@@ -11,7 +11,7 @@ import numpy as np
11 import torch
12 from PIL import Image
13
14 -import vllm.envs as envs
14 +from vllm.config.multimodal import MMHasherAlgorithm
15 from vllm.logger import init_logger
16
17 from .media import MediaWithBytes
@@ -20,7 +20,9 @@ logger = init_logger(__name__)
20
21
22 @functools.lru_cache(maxsize=3)
23 -def _get_hasher_factory(algorithm: str) -> Callable[[], "hashlib._Hash"]:
23 +def _get_hasher_factory(
24 + algorithm: MMHasherAlgorithm,
25 +) -> Callable[[], "hashlib._Hash"]:
26 """
27 Get the hasher factory based on the configured algorithm.
28
@@ -32,7 +34,6 @@ def _get_hasher_factory(algorithm: str) -> Callable[[], "hashlib._Hash"]:
34
35 See: https://github.com/vllm-project/vllm/issues/18334
36 """
35 - algorithm = algorithm.lower()
37
38 if algorithm == "blake3":
39 from blake3 import blake3
@@ -43,7 +44,7 @@ def _get_hasher_factory(algorithm: str) -> Callable[[], "hashlib._Hash"]:
44 elif algorithm == "sha512":
45 return hashlib.sha512
46 else:
46 - # This should never happen due to env_with_choices validation
47 + # This should never happen due to config validation
48 raise ValueError(f"Unsupported hash algorithm: {algorithm}")
49
50
@@ -162,8 +163,13 @@ class MultiModalHasher:
163 yield from cls.serialize_item(obj)
164
165 @classmethod
165 - def hash_kwargs(cls, **kwargs: object) -> str:
166 - hasher_factory = _get_hasher_factory(envs.VLLM_MM_HASHER_ALGORITHM)
166 + def hash_kwargs(
167 + cls,
168 + algorithm: MMHasherAlgorithm,
169 + /,
170 + **kwargs: object,
171 + ) -> str:
172 + hasher_factory = _get_hasher_factory(algorithm)
173 hasher = hasher_factory()
174
175 for k, v in sorted(kwargs.items(), key=lambda kv: kv[0]):
vllm/multimodal/inputs.py
+24 -7
@@ -229,40 +229,57 @@ Uses a list instead of a tensor if the dimensions of each element do not match.
229 """
230
231
232 -def nested_tensors_equal(a: NestedTensors, b: NestedTensors) -> bool:
232 +def nested_tensors_equal(
233 + a: NestedTensors,
234 + b: NestedTensors,
235 + check_dtype: bool = True,
236 +) -> bool:
237 """
238 Equality check between
239 [`NestedTensors`][vllm.multimodal.inputs.NestedTensors] objects.
240 +
241 + If `check_dtype` is `True`, the tensors must have the same dtype.
242 """
243 + check_dtype_func = (
244 + lambda a, b, check_dtype: a.dtype == b.dtype if check_dtype else True
245 + )
246 if isinstance(a, torch.Tensor):
238 - return isinstance(b, torch.Tensor) and torch.equal(a, b)
247 + return (
248 + isinstance(b, torch.Tensor)
249 + and torch.equal(a, b)
250 + and check_dtype_func(a, b, check_dtype)
251 + )
252 elif isinstance(b, torch.Tensor):
240 - return isinstance(a, torch.Tensor) and torch.equal(b, a)
253 + return (
254 + isinstance(a, torch.Tensor)
255 + and torch.equal(b, a)
256 + and check_dtype_func(b, a, check_dtype)
257 + )
258
259 if isinstance(a, list):
260 return (
261 isinstance(b, list)
262 and len(a) == len(b)
246 - and all(nested_tensors_equal(a_, b_) for a_, b_ in zip(a, b))
263 + and all(nested_tensors_equal(a_, b_, check_dtype) for a_, b_ in zip(a, b))
264 )
265 if isinstance(b, list):
266 return (
267 isinstance(a, list)
268 and len(b) == len(a)
252 - and all(nested_tensors_equal(b_, a_) for b_, a_ in zip(b, a))
269 + and all(nested_tensors_equal(b_, a_, check_dtype) for b_, a_ in zip(b, a))
270 )
271
272 if isinstance(a, tuple):
273 return (
274 isinstance(b, tuple)
275 and len(a) == len(b)
259 - and all(nested_tensors_equal(a_, b_) for a_, b_ in zip(a, b))
276 + and all(nested_tensors_equal(a_, b_, check_dtype) for a_, b_ in zip(a, b))
277 )
278 if isinstance(b, tuple):
279 return (
280 isinstance(a, tuple)
281 and len(b) == len(a)
265 - and all(nested_tensors_equal(b_, a_) for b_, a_ in zip(b, a))
282 + and all(nested_tensors_equal(b_, a_, check_dtype) for b_, a_ in zip(b, a))
283 )
284
285 # Both a and b are scalars
vllm/multimodal/processing/inputs.py
+8 -1
@@ -3,6 +3,7 @@
3 from collections.abc import Mapping
4 from dataclasses import dataclass, field
5
6 +from vllm.config.multimodal import MMHasherAlgorithm
7 from vllm.inputs import MultiModalHashes
8
9 from ..hasher import MultiModalHasher
@@ -22,7 +23,11 @@ class ProcessorInputs:
23 hf_processor_mm_kwargs: Mapping[str, object] = field(default_factory=dict)
24 tokenization_kwargs: Mapping[str, object] = field(default_factory=dict)
25
25 - def get_mm_hashes(self, model_id: str) -> MultiModalHashes:
26 + def get_mm_hashes(
27 + self,
28 + model_id: str,
29 + hash_algorithm: MMHasherAlgorithm,
30 + ) -> MultiModalHashes:
31 mm_data_items = self.mm_data_items
32 mm_uuid_items = self.mm_uuid_items or {}
33 hf_processor_mm_kwargs = self.hf_processor_mm_kwargs
@@ -49,6 +54,7 @@ class ProcessorInputs:
54 item = uuid_item if uuid_item is not None else item
55 hashes.append(
56 hasher.hash_kwargs(
57 + hash_algorithm,
58 model_id=model_id,
59 **{modality: item},
60 **hf_processor_mm_kwargs,
@@ -61,6 +67,7 @@ class ProcessorInputs:
67 else:
68 mm_hashes[modality] = [
69 hasher.hash_kwargs(
70 + hash_algorithm,
71 model_id=model_id,
72 **{modality: item},
73 **hf_processor_mm_kwargs,
vllm/multimodal/processing/processor.py
+8 -2
@@ -1422,7 +1422,10 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1422
1423 # Use overrides if provided; fallback to data-dependent hashing.
1424 with timing_ctx.record("get_mm_hashes"):
1425 - mm_hashes = inputs.get_mm_hashes(self.info.model_id)
1425 + mm_hashes = inputs.get_mm_hashes(
1426 + self.info.model_id,
1427 + self.info.ctx.get_mm_config().mm_hasher_algorithm,
1428 + )
1429
1430 mm_prompt_updates = self._get_mm_prompt_updates(
1431 inputs.mm_data_items,
@@ -1454,7 +1457,10 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1457 return self._apply_hf_processor(inputs, timing_ctx)
1458
1459 with timing_ctx.record("get_mm_hashes"):
1457 - mm_hashes = inputs.get_mm_hashes(self.info.model_id)
1460 + mm_hashes = inputs.get_mm_hashes(
1461 + self.info.model_id,
1462 + self.info.ctx.get_mm_config().mm_hasher_algorithm,
1463 + )
1464
1465 with timing_ctx.record("get_cache_missing_items"):
1466 mm_is_cached, mm_missing_data_items = self._get_cache_missing_items(
vllm/multimodal/utils.py
+31 -19
@@ -16,13 +16,13 @@ from typing_extensions import deprecated
16 from vllm.inputs import MultiModalPlaceholders
17 from vllm.utils.import_utils import LazyLoader
18
19 -from .hasher import MultiModalHasher
19 from .inputs import (
20 BatchedTensorInputs,
21 MultiModalFeatureSpec,
22 MultiModalFieldElem,
23 MultiModalKwargsItem,
24 MultiModalSharedField,
25 + nested_tensors_equal,
26 )
27 from .media import (
28 AudioMediaIO,
@@ -165,11 +165,28 @@ def argsort_mm_positions(
165 return [(modality, idx) for modality, idx, _ in sorted_flat_items]
166
167
168 -def _get_group_hash(elem: MultiModalFieldElem):
169 - if not isinstance(elem.field, MultiModalSharedField):
170 - return None
168 +def _can_batch_mm_items(
169 + left: MultiModalKwargsItem,
170 + right: MultiModalKwargsItem,
171 +) -> bool:
172 + if left.keys() != right.keys():
173 + return False
174
172 - return MultiModalHasher.hash_kwargs(data=elem.data)
175 + for key, left_elem in left.items():
176 + right_elem = right[key]
177 + left_field, right_field = left_elem.field, right_elem.field
178 + is_shared_field = isinstance(left_field, MultiModalSharedField) and isinstance(
179 + right_field, MultiModalSharedField
180 + )
181 + if (type(left_field) is not type(right_field)) or (
182 + is_shared_field
183 + and not nested_tensors_equal(
184 + left_elem.data, right_elem.data, check_dtype=True
185 + )
186 + ):
187 + return False
188 +
189 + return True
190
191
192 def _batch_mm_items(
@@ -217,26 +234,21 @@ def group_and_batch_mm_items(
234 - `kwargs` is a dictionary of keyword arguments to pass to the model;
235 - `num_items` is the corresponding number of items.
236 """
220 - group_ids = [
221 - tuple(
222 - (key, _get_group_hash(elem))
223 - for key, elem in sorted(item.items(), key=lambda kv: kv[0])
224 - )
225 - for item in items
226 - ]
227 - group_sizes = [sum(1 for _ in group) for _, group in groupby(group_ids)]
228 -
237 start_idx = 0
230 - for group_size in group_sizes:
238 + for end_idx in range(1, len(items) + 1):
239 + if end_idx < len(items) and _can_batch_mm_items(
240 + items[end_idx - 1], items[end_idx]
241 + ):
242 + continue
243 +
244 group_data = _batch_mm_items(
232 - items[start_idx : start_idx + group_size],
245 + items[start_idx:end_idx],
246 device=device,
247 pin_memory=pin_memory,
248 )
249
237 - yield group_size, group_data
238 -
239 - start_idx += group_size
250 + yield end_idx - start_idx, group_data
251 + start_idx = end_idx
252
253 assert start_idx == len(items)
254
vllm/renderers/hf.py
+7 -2
@@ -1276,8 +1276,8 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
1276 embeds_prompt["prompt_embeds"] = full_embeds
1277 embeds_prompt["prompt_is_token_ids"] = is_token_ids_mask
1278
1279 - @staticmethod
1279 def _apply_prompt_embeds_to_engine_input(
1280 + self,
1281 engine_input: MultiModalInput,
1282 prompt_embeds_tensors: list[torch.Tensor],
1283 mm_updates: MultiModalPromptUpdates,
@@ -1300,6 +1300,7 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
1300 pe_kwargs_items: list[MultiModalKwargsItem] = []
1301 pe_hashes: list[str] = []
1302 pe_placeholders: list[PlaceholderRange] = []
1303 + mm_config = self.model_config.get_multimodal_config()
1304 for tensor, (start, length) in zip(
1305 prompt_embeds_tensors, positions, strict=True
1306 ):
@@ -1313,7 +1314,11 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
1314 }
1315 )
1316 )
1316 - pe_hashes.append(MultiModalHasher.hash_kwargs(prompt_embeds=tensor))
1317 + pe_hashes.append(
1318 + MultiModalHasher.hash_kwargs(
1319 + mm_config.mm_hasher_algorithm, prompt_embeds=tensor
1320 + )
1321 + )
1322 # `is_embed=None` matches the existing image_embeds-style
1323 # "no encoder, just splice the tensor directly" semantics.
1324 pe_placeholders.append(