[Bugfix][Multimodal] Include media IO config in MM cache hash (#49975)
Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com>
Guan-Ming Chiu committed
Jul 29, 2026 at 18:48 UTC
aeaa50a71c4f53a8b38ae043ab5914fed2504d4a
5 files changed
+54
-3
tests/multimodal/test_hasher.py
+36
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import uuid
4
+from io import BytesIO
5
from pathlib import Path
6
7
import numpy as np
@@ -10,6 +11,7 @@ from PIL import Image, ImageDraw
11
12
from vllm.multimodal.hasher import MultiModalHasher
13
from vllm.multimodal.media.base import MediaWithBytes
14
+from vllm.multimodal.media.image import ImageMediaIO
15
from vllm.multimodal.parse import MultiModalDataParser
16
17
pytestmark = pytest.mark.cpu_test
@@ -134,3 +136,37 @@ def test_hash_image_exif_id():
136
assert hasher.hash_kwargs(image=image1) == hasher.hash_kwargs(image=id.bytes)
137
# 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)
139
+
140
+
141
+def _rgba_png_bytes() -> bytes:
142
+ image = Image.new("RGBA", (8, 8), (255, 0, 0, 128))
143
+ buf = BytesIO()
144
+ image.save(buf, format="PNG")
145
+ return buf.getvalue()
146
+
147
+
148
+def test_hash_collision_media_io_config():
149
+ data = _rgba_png_bytes()
150
+ white = ImageMediaIO(rgba_background_color=(255, 255, 255)).load_bytes(data)
151
+ black = ImageMediaIO(rgba_background_color=(0, 0, 0)).load_bytes(data)
152
+ white2 = ImageMediaIO(rgba_background_color=(255, 255, 255)).load_bytes(data)
153
+ keep = ImageMediaIO(image_mode=None).load_bytes(data)
154
+
155
+ 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)
159
+
160
+
161
+def test_hash_media_io_noop_config_preserves_hash():
162
+ image = Image.new("RGB", (8, 8), (0, 128, 255))
163
+ buf = BytesIO()
164
+ image.save(buf, format="PNG")
165
+ data = buf.getvalue()
166
+
167
+ loaded = ImageMediaIO().load_bytes(data)
168
+ assert loaded.io_config is None
169
+
170
+ plain = MediaWithBytes(loaded.media, data)
171
+ hasher = MultiModalHasher
172
+ assert hasher.hash_kwargs(image=loaded) == hasher.hash_kwargs(image=plain)
vllm/multimodal/hasher.py
+5
@@ -81,6 +81,11 @@ class MultiModalHasher:
81
):
82
return (exif[Image.ExifTags.Base.ImageID].bytes,)
83
84
+ if obj.io_config:
85
+ return cls.iter_item_to_bytes(
86
+ "image",
87
+ {"io_config": obj.io_config, "data": obj.original_bytes},
88
+ )
89
return cls.iter_item_to_bytes("image", obj.original_bytes)
90
91
if isinstance(obj, MediaWithBytes) and isinstance(obj.media, np.ndarray):
vllm/multimodal/media/base.py
+3
@@ -29,6 +29,9 @@ class MediaWithBytes(Generic[_T]):
29
30
media: _T
31
original_bytes: bytes = field(repr=False)
32
+ io_config: dict[str, Any] | None = None
33
+ """Decode settings that altered the media relative to `original_bytes`
34
+ (e.g. `image_mode` conversion), so they participate in cache hashing."""
35
36
def __array__(self, *args, **kwargs) -> np.ndarray:
37
"""Allow np.array(obj) to return np.array(obj.media)."""
vllm/multimodal/media/image.py
+9
-2
@@ -86,10 +86,17 @@ class ImageMediaIO(MediaIO[Image.Image]):
86
)
87
image = normalize_image(image)
88
image.load()
89
- image = self._convert_image_mode(image)
89
+ converted = self._convert_image_mode(image)
90
except (OSError, Image.UnidentifiedImageError) as e:
91
raise ValueError(f"Failed to load image: {e}") from e
92
- return MediaWithBytes(image, data)
92
+
93
+ io_config = None
94
+ if converted is not image:
95
+ io_config = {
96
+ "image_mode": self.image_mode,
97
+ "rgba_background_color": self.rgba_background_color,
98
+ }
99
+ return MediaWithBytes(converted, data, io_config)
100
101
def load_base64(self, media_type: str, data: str) -> MediaWithBytes[Image.Image]:
102
return self.load_bytes(pybase64.b64decode(data, validate=True))
vllm/multimodal/processing/inputs.py
+1
-1
@@ -65,7 +65,7 @@ class ProcessorInputs:
65
**{modality: item},
66
**hf_processor_mm_kwargs,
67
)
68
- for item in data_items
68
+ for item in data_items.get_all_items_for_hash()
69
]
70
71
return mm_hashes