A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 10, 16:30 UTC
Fix RecursionError in MediaWithBytes unpickling (#31191)
Signed-off-by: Nikhil Ghosh <nikhil@anyscale.com>
Nikhil G committed
Jan 6, 2026 at 12:11 UTC
ada6f91d561ab693fd85f028bc44b8c8058d3073
3 files changed
+39
-1
tests/multimodal/test_image.py
+33
@@ -1,11 +1,13 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import pickle
4
from pathlib import Path
5
6
import numpy as np
7
import pytest
8
from PIL import Image, ImageChops
9
10
+from vllm.multimodal.base import MediaWithBytes
11
from vllm.multimodal.image import ImageMediaIO, convert_image_mode
12
13
pytestmark = pytest.mark.cpu_test
@@ -157,3 +159,34 @@ def test_rgba_background_color_validation():
159
ImageMediaIO(rgba_background_color=(0, 0, 0)) # Should not raise
160
ImageMediaIO(rgba_background_color=[255, 255, 255]) # Should not raise
161
ImageMediaIO(rgba_background_color=(128, 128, 128)) # Should not raise
162
+
163
+
164
+def test_media_with_bytes_pickle_roundtrip():
165
+ """Regression test for pickle/unpickle of MediaWithBytes.
166
+
167
+ Verifies that MediaWithBytes can be pickled and unpickled without
168
+ RecursionError. See: https://github.com/vllm-project/vllm/issues/30818
169
+ """
170
+ original_image = Image.open(ASSETS_DIR / "image1.png").convert("RGB")
171
+ original_bytes = b"test_bytes_data"
172
+
173
+ wrapper = MediaWithBytes(media=original_image, original_bytes=original_bytes)
174
+
175
+ # Verify attribute delegation works before pickling
176
+ assert wrapper.width == original_image.width
177
+ assert wrapper.height == original_image.height
178
+ assert wrapper.mode == original_image.mode
179
+
180
+ # Pickle and unpickle (this would cause RecursionError before the fix)
181
+ pickled = pickle.dumps(wrapper)
182
+ unpickled = pickle.loads(pickled)
183
+
184
+ # Verify the unpickled object works correctly
185
+ assert unpickled.original_bytes == original_bytes
186
+ assert unpickled.media.width == original_image.width
187
+ assert unpickled.media.height == original_image.height
188
+
189
+ # Verify attribute delegation works after unpickling
190
+ assert unpickled.width == original_image.width
191
+ assert unpickled.height == original_image.height
192
+ assert unpickled.mode == original_image.mode
tools/pre_commit/check_pickle_imports.py
+1
@@ -27,6 +27,7 @@ ALLOWED_FILES = {
27
"vllm/distributed/device_communicators/shm_broadcast.py",
28
"vllm/distributed/device_communicators/shm_object_storage.py",
29
"vllm/utils/hashing.py",
30
+ "tests/multimodal/test_image.py",
31
"tests/tokenizers_/test_hf.py",
32
"tests/utils_/test_hashing.py",
33
"benchmarks/kernels/graph_machete_bench.py",
vllm/multimodal/base.py
+5
-1
@@ -34,7 +34,11 @@ class MediaWithBytes(Generic[_T]):
34
35
def __getattr__(self, name: str):
36
"""Delegate attribute access to the underlying media object."""
37
- # This is only called when the attribute is not found on self
37
+ # Guard against recursion during unpickling when media isn't set yet.
38
+ # pickle creates objects without calling __init__, so self.media may
39
+ # not exist when __getattr__ is called for methods like __setstate__.
40
+ if "media" not in self.__dict__:
41
+ raise AttributeError(name)
42
return getattr(self.media, name)
43
44