Skip to content

Commit 1180b60

Browse files
authored
[Multimodal] Expose mm hash algothrim selection to cli args (vllm-project#49686)
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
1 parent 0f17394 commit 1180b60

17 files changed

Lines changed: 234 additions & 61 deletions

File tree

docs/usage/security.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ FIPS compliance depends on many factors, so a vLLM deployment is not automatical
396396

397397
Operators running vLLM on FIPS-enabled hosts should select FIPS-approved algorithms via the following knobs:
398398

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.
400400
- **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.
401401
- **TLS ciphers** — use `--ssl-ciphers` to restrict the API server's TLS handshake to FIPS-approved cipher suites that match your environment's policy.
402402

@@ -408,7 +408,13 @@ vLLM uses MD5 in a few places to derive non-security cache keys (for example, co
408408

409409
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:
410410

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.
412418
- `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.
413419

414420
### Beyond hashing: other FIPS considerations

tests/config/test_multimodal_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def test_mm_encoder_attn_backend_invalid():
2424
MultiModalConfig(mm_encoder_attn_backend="not_a_backend") # type: ignore[arg-type]
2525

2626

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+
2732
def test_mm_encoder_attn_backend_hash_updates():
2833
base_hash = MultiModalConfig().compute_hash()
2934
overridden_hash = MultiModalConfig(

tests/models/multimodal/processing/test_moss_audio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def batch_decode(self, batch_token_ids, **kwargs):
5050
class _MMConfig:
5151
enable_mm_embeds = False
5252
mm_processor_cache_gb = 1
53+
mm_hasher_algorithm = "blake3"
5354

5455
def merge_mm_processor_kwargs(self, kwargs):
5556
return dict(kwargs)

tests/multimodal/test_cache.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ def _compare_caches(
144144
for _ in range(int(item_capacity / hit_rate))
145145
]
146146
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
148149
]
149150

150151
prompt_update = PromptInsertion("dummy", "target", "insertion").resolve(0)

tests/multimodal/test_hasher.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
import hashlib
34
import uuid
45
from io import BytesIO
56
from pathlib import Path
@@ -9,6 +10,7 @@
910
import torch
1011
from PIL import Image, ImageDraw
1112

13+
from vllm.config.multimodal import MMHasherAlgorithm
1214
from vllm.multimodal.hasher import MultiModalHasher
1315
from vllm.multimodal.media.base import MediaWithBytes
1416
from vllm.multimodal.media.image import ImageMediaIO
@@ -20,20 +22,36 @@
2022
assert ASSETS_DIR.exists()
2123

2224

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+
2339
def test_hash_single_item_different_shape():
2440
x1 = torch.zeros(())
2541
x2 = torch.zeros((1,))
2642

2743
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)
2945

3046

3147
def test_hash_key_order_invariant():
3248
x = torch.zeros((5, 10))
3349
y = torch.ones((5, 10))
3450

3551
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+
)
3755

3856

3957
# 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):
4462
image2 = Image.new(mode2, size=(10, 10), color=1)
4563

4664
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+
)
4868

4969

5070
def test_hash_collision_image_palette():
@@ -53,7 +73,9 @@ def test_hash_collision_image_palette():
5373
image2 = Image.open(ASSETS_DIR / "image2.png")
5474

5575
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+
)
5779

5880

5981
def test_hash_collision_image_transpose():
@@ -64,7 +86,9 @@ def test_hash_collision_image_transpose():
6486
ImageDraw.Draw(image2).line([(0, 0), (0, 10)])
6587

6688
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+
)
6892

6993

7094
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
@@ -74,7 +98,9 @@ def test_hash_collision_tensor_shape(dtype):
7498
arr2 = torch.zeros((10, 20, 5, 3), dtype=dtype)
7599

76100
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+
)
78104

79105

80106
def test_hash_collision_array_shape():
@@ -83,7 +109,9 @@ def test_hash_collision_array_shape():
83109
arr2 = np.zeros((10, 20, 5, 3))
84110

85111
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+
)
87115

88116

89117
def test_hash_collision_video_num_frames():
@@ -104,8 +132,8 @@ def item_for_hash(num_frames: int):
104132
return items.get_all_items_for_hash()[0]
105133

106134
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)
109137
)
110138

111139

@@ -118,7 +146,9 @@ def test_hash_non_contiguous_array():
118146

119147
hasher = MultiModalHasher
120148
# 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+
)
122152

123153

124154
def test_hash_image_exif_id():
@@ -133,9 +163,13 @@ def test_hash_image_exif_id():
133163

134164
hasher = MultiModalHasher
135165
# 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+
)
137169
# 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+
)
139173

140174

141175
def _rgba_png_bytes() -> bytes:
@@ -153,9 +187,15 @@ def test_hash_collision_media_io_config():
153187
keep = ImageMediaIO(image_mode=None).load_bytes(data)
154188

155189
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+
)
159199

160200

161201
def test_hash_media_io_noop_config_preserves_hash():
@@ -169,4 +209,6 @@ def test_hash_media_io_noop_config_preserves_hash():
169209

170210
plain = MediaWithBytes(loaded.media, data)
171211
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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,31 @@ def test_group_and_batch_mm_items_split_by_fieldset():
199199
assert [num_items for num_items, _ in res] == [2, 1, 1, 1]
200200

201201

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+
202227
def test_group_and_batch_mm_items_split_by_shared_data():
203228
elem1 = MultiModalFieldElem(
204229
data=torch.zeros(1, dtype=torch.uint8),

vllm/config/model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from vllm.config.multimodal import (
1818
MMCacheType,
1919
MMEncoderTPMode,
20+
MMHasherAlgorithm,
2021
MMTensorIPC,
2122
MultiModalConfig,
2223
)
@@ -374,6 +375,7 @@ class ModelConfig:
374375
mm_processor_kwargs: InitVar[dict[str, Any] | None] = None
375376
mm_processor_cache_gb: InitVar[float | None] = None
376377
mm_processor_cache_type: InitVar[MMCacheType | None] = None
378+
mm_hasher_algorithm: InitVar[MMHasherAlgorithm | None] = None
377379
mm_shm_cache_max_object_size_mb: InitVar[int | None] = None
378380
mm_encoder_only: InitVar[bool | None] = None
379381
mm_encoder_tp_mode: InitVar[MMEncoderTPMode | None] = None
@@ -503,6 +505,7 @@ def __post_init__(
503505
mm_processor_kwargs: dict[str, Any] | None,
504506
mm_processor_cache_gb: float | None,
505507
mm_processor_cache_type: MMCacheType | None,
508+
mm_hasher_algorithm: MMHasherAlgorithm | None,
506509
mm_shm_cache_max_object_size_mb: int | None,
507510
mm_encoder_only: bool | None,
508511
mm_encoder_tp_mode: MMEncoderTPMode | None,
@@ -737,6 +740,7 @@ def __post_init__(
737740
mm_processor_kwargs=mm_processor_kwargs,
738741
mm_processor_cache_gb=mm_processor_cache_gb,
739742
mm_processor_cache_type=mm_processor_cache_type,
743+
mm_hasher_algorithm=mm_hasher_algorithm,
740744
mm_shm_cache_max_object_size_mb=mm_shm_cache_max_object_size_mb,
741745
mm_encoder_only=mm_encoder_only,
742746
mm_encoder_tp_mode=mm_encoder_tp_mode,

vllm/config/multimodal.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
from collections.abc import Mapping
55
from pathlib import Path
6-
from typing import Any, Literal, TypeAlias, TypedDict, final
6+
from typing import Any, Literal, TypeAlias, TypedDict, cast, final
77

88
from pydantic import ConfigDict, Field, field_validator, model_validator
99
from pydantic.dataclasses import dataclass
1010

1111
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
1313
from vllm.utils.hashing import safe_hash
1414
from vllm.v1.attention.backends.registry import AttentionBackendEnum
1515

@@ -63,6 +63,19 @@ class MultiModalDummyOptionsBuiltins(TypedDict, total=False):
6363
MMCacheType = Literal["shm", "lru"]
6464
VideoPruningMethod = Literal["evs", "vidcom2"]
6565
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+
6679
MMDummyOptions: TypeAlias = dict[str, BaseDummyOptions]
6780
"""
6881
A dictionary containing an entry for each modality type of dummy data.
@@ -134,6 +147,11 @@ class MultiModalConfig:
134147
mm_processor_cache_type: MMCacheType = "lru"
135148
"""Type of cache to use for the multi-modal preprocessor/mapper. If `shm`,
136149
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."""
137155
mm_shm_cache_max_object_size_mb: int = Field(default=128, ge=0)
138156
"""Size limit (in MiB) for each object stored in the multi-modal processor
139157
shared memory cache. Only effective when `mm_processor_cache_type` is

vllm/engine/arg_utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@
8686
RunnerOption,
8787
TokenizerMode,
8888
)
89-
from vllm.config.multimodal import MMCacheType, MMEncoderTPMode, MMTensorIPC
89+
from vllm.config.multimodal import (
90+
MMCacheType,
91+
MMEncoderTPMode,
92+
MMHasherAlgorithm,
93+
MMTensorIPC,
94+
)
9095
from vllm.config.observability import DetailedTraceModules
9196
from vllm.config.parallel import (
9297
All2AllBackend,
@@ -566,6 +571,9 @@ class EngineArgs:
566571
mm_processor_cache_type: MMCacheType | None = (
567572
MultiModalConfig.mm_processor_cache_type
568573
)
574+
mm_hasher_algorithm: MMHasherAlgorithm = get_field(
575+
MultiModalConfig, "mm_hasher_algorithm"
576+
)
569577
mm_shm_cache_max_object_size_mb: int = (
570578
MultiModalConfig.mm_shm_cache_max_object_size_mb
571579
)
@@ -1295,6 +1303,9 @@ def add_cli_args(parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
12951303
multimodal_group.add_argument(
12961304
"--mm-processor-cache-type", **multimodal_kwargs["mm_processor_cache_type"]
12971305
)
1306+
multimodal_group.add_argument(
1307+
"--mm-hasher-algorithm", **multimodal_kwargs["mm_hasher_algorithm"]
1308+
)
12981309
multimodal_group.add_argument(
12991310
"--mm-shm-cache-max-object-size-mb",
13001311
**multimodal_kwargs["mm_shm_cache_max_object_size_mb"],
@@ -1712,6 +1723,7 @@ def create_model_config(self) -> ModelConfig:
17121723
mm_processor_cache_gb=self.mm_processor_cache_gb,
17131724
mm_processor_cache_type=self.mm_processor_cache_type,
17141725
mm_shm_cache_max_object_size_mb=self.mm_shm_cache_max_object_size_mb,
1726+
mm_hasher_algorithm=self.mm_hasher_algorithm,
17151727
mm_encoder_only=self.mm_encoder_only,
17161728
mm_encoder_tp_mode=self.mm_encoder_tp_mode,
17171729
mm_encoder_attn_backend=self.mm_encoder_attn_backend,

vllm/model_executor/models/terratorch.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ def apply(
217217
)
218218

219219
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+
)
221224

222225
mm_placeholders = {"image": [PlaceholderRange(offset=0, length=0)]}
223226

0 commit comments

Comments
 (0)