@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / ada6f91d56
Fork 0

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