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 14, 05:30 UTC
[CI/Build] Serve images used by multimodal tests through local HTTP Server (#23907)
Signed-off-by: Divyansh Singhvi <divyanshsinghvi@gmail.com> Signed-off-by: dsinghvi <divyanshsinghvi@gmail.com> Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
dsinghvi committed
Sep 3, 2025 at 13:43 UTC
70549c1245c3eeb3706e3c09a9e18d702fbf705f
9 files changed
+250
-98
tests/conftest.py
+122
@@ -1,9 +1,14 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import http.server
4
import json
5
import math
6
+import mimetypes
7
import os
8
+import socket
9
import tempfile
10
+import threading
11
+from collections.abc import Generator
12
from enum import Enum
13
from typing import Any, Callable, Optional, TypedDict, TypeVar, Union, cast
14
@@ -32,6 +37,7 @@ from vllm.distributed import (cleanup_dist_env_and_memory,
37
from vllm.inputs import (ExplicitEncoderDecoderPrompt, TextPrompt,
38
to_enc_dec_tuple_list, zip_enc_dec_prompts)
39
from vllm.logger import init_logger
40
+from vllm.multimodal.utils import fetch_image
41
from vllm.outputs import RequestOutput
42
from vllm.sampling_params import BeamSearchParams
43
from vllm.sequence import Logprob
@@ -1253,3 +1259,119 @@ def cli_config_file():
1259
def cli_config_file_with_model():
1260
"""Return the path to the CLI config file with model."""
1261
return os.path.join(_TEST_DIR, "config", "test_config_with_model.yaml")
1262
+
1263
+
1264
+class AssetHandler(http.server.BaseHTTPRequestHandler):
1265
+ # _IMAGE_CACHE : Dict[str, bytes] = {}
1266
+
1267
+ def log_message(self, *args, **kwargs):
1268
+ pass
1269
+
1270
+ def do_GET(self):
1271
+ # Accepts paths like: /1280px-Venn_diagram_rgb.jpg
1272
+ filename = self.path.lstrip("/")
1273
+ if not filename or "." not in filename:
1274
+ self.send_error(404, "Missing filename (expected /<name>.<ext>)")
1275
+ return
1276
+
1277
+ base, ext = filename.rsplit(".", 1)
1278
+ ext = ext.lower()
1279
+
1280
+ if ext not in ["jpg", "png"]:
1281
+ self.send_error(404, f"Unsupported extension: .{ext}")
1282
+ return
1283
+
1284
+ try:
1285
+ data = ImageAsset(base).read_bytes(ext=ext)
1286
+ except Exception as e:
1287
+ self.send_error(500, f"Failed to load asset: {ext} {base} {e} ")
1288
+ return
1289
+
1290
+ ctype, _ = mimetypes.guess_type(filename)
1291
+ if ctype is None:
1292
+ ctype = {"jpg": "image/jpg", "png": "image/png"}[ext]
1293
+ self.send_response(200)
1294
+ self.send_header("Content-Type", ctype)
1295
+ self.send_header("Content-Length", str(len(data)))
1296
+ self.end_headers()
1297
+ self.wfile.write(data)
1298
+
1299
+
1300
+def _find_free_port() -> int:
1301
+ with socket.socket() as s:
1302
+ s.bind(("127.0.0.1", 0))
1303
+ return s.getsockname()[1]
1304
+
1305
+
1306
+class LocalAssetServer:
1307
+
1308
+ address: str
1309
+ port: int
1310
+ server: Optional[http.server.ThreadingHTTPServer]
1311
+ thread: Optional[threading.Thread]
1312
+
1313
+ def __init__(self, address: str = "127.0.0.1") -> None:
1314
+ self.address = address
1315
+ self.port = -1
1316
+ self.server = None
1317
+ self.thread = None
1318
+
1319
+ def __enter__(self):
1320
+ self.port = _find_free_port()
1321
+ self.server = http.server.ThreadingHTTPServer(
1322
+ (self.address, self.port), AssetHandler)
1323
+ self.thread = threading.Thread(target=self.server.serve_forever,
1324
+ daemon=True)
1325
+ self.thread.start()
1326
+ return self
1327
+
1328
+ def __exit__(self, exc_type, exc_value, traceback):
1329
+ if self.server:
1330
+ self.server.shutdown()
1331
+ del self.server
1332
+
1333
+ if self.thread:
1334
+ self.thread.join()
1335
+ del self.thread
1336
+
1337
+ if exc_type is None:
1338
+ return None
1339
+
1340
+ return False
1341
+
1342
+ @property
1343
+ def base_url(self) -> str:
1344
+ assert self.port is not None
1345
+ return f"http://{self.address}:{self.port}"
1346
+
1347
+ def url_for(self, name: str) -> str:
1348
+ """e.g., name='RGBA_comp.png' -> 'http://127.0.0.1:PORT/RGBA_comp.png'"""
1349
+ return f"{self.base_url}/{name}"
1350
+
1351
+ def get_image_asset(self, name: str) -> Image.Image:
1352
+ return fetch_image(self.url_for(name))
1353
+
1354
+
1355
+@pytest.fixture(scope="session")
1356
+def local_asset_server() -> Generator[LocalAssetServer, None, None]:
1357
+ """
1358
+ Starts a thread based HTTP server bound to 127.0.0.1 on a random free port.
1359
+ The server currently servers images at:
1360
+ http://127.0.0.1:<port>/<name>.<ext>
1361
+ """
1362
+ with LocalAssetServer() as srv:
1363
+ yield srv
1364
+
1365
+
1366
+@pytest.fixture
1367
+def image_url(request, local_asset_server) -> str:
1368
+ # request.param is one of the IMAGE_ASSETS filenames
1369
+ name = request.param
1370
+ return local_asset_server.url_for(name)
1371
+
1372
+
1373
+@pytest.fixture
1374
+def image_urls(request, local_asset_server) -> list[str]:
1375
+ """Indirect fixture: takes a list of names, returns list of full URLs."""
1376
+ names: list[str] = request.param
1377
+ return [local_asset_server.url_for(name) for name in names]
tests/entrypoints/llm/test_chat.py
+3
-2
@@ -7,7 +7,7 @@ import pytest
7
from vllm import LLM
8
from vllm.distributed import cleanup_dist_env_and_memory
9
10
-from ..openai.test_vision import TEST_IMAGE_URLS
10
+from ..openai.test_vision import TEST_IMAGE_ASSETS
11
12
13
@pytest.fixture(scope="function")
@@ -95,7 +95,8 @@ def vision_llm():
95
96
97
@pytest.mark.parametrize("image_urls",
98
- [[TEST_IMAGE_URLS[0], TEST_IMAGE_URLS[1]]])
98
+ [[TEST_IMAGE_ASSETS[0], TEST_IMAGE_ASSETS[1]]],
99
+ indirect=True)
100
def test_chat_multi_image(vision_llm, image_urls: list[str]):
101
messages = [{
102
"role":
tests/entrypoints/openai/test_vision.py
+23
-20
@@ -16,11 +16,11 @@ MODEL_NAME = "microsoft/Phi-3.5-vision-instruct"
16
MAXIMUM_IMAGES = 2
17
18
# Test different image extensions (JPG/PNG) and formats (gray/RGB/RGBA)
19
-TEST_IMAGE_URLS = [
20
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
21
- "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
22
- "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
23
- "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
19
+TEST_IMAGE_ASSETS = [
20
+ "2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", # "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
21
+ "Grayscale_8bits_palette_sample_image.png", # "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
22
+ "1280px-Venn_diagram_rgb.svg.png", # "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
23
+ "RGBA_comp.png", # "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
24
]
25
26
EXPECTED_MM_BEAM_SEARCH_RES = [
@@ -69,10 +69,11 @@ async def client(server):
69
70
71
@pytest.fixture(scope="session")
72
-def base64_encoded_image() -> dict[str, str]:
72
+def base64_encoded_image(local_asset_server) -> dict[str, str]:
73
return {
74
- image_url: encode_image_base64(fetch_image(image_url))
75
- for image_url in TEST_IMAGE_URLS
74
+ image_asset:
75
+ encode_image_base64(local_asset_server.get_image_asset(image_asset))
76
+ for image_asset in TEST_IMAGE_ASSETS
77
}
78
79
@@ -97,7 +98,7 @@ def get_hf_prompt_tokens(model_name, content, image_url):
98
99
@pytest.mark.asyncio
100
@pytest.mark.parametrize("model_name", [MODEL_NAME])
100
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
101
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
102
async def test_single_chat_session_image(client: openai.AsyncOpenAI,
103
model_name: str, image_url: str):
104
content_text = "What's in this image?"
@@ -157,7 +158,7 @@ async def test_single_chat_session_image(client: openai.AsyncOpenAI,
158
159
@pytest.mark.asyncio
160
@pytest.mark.parametrize("model_name", [MODEL_NAME])
160
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
161
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
162
async def test_error_on_invalid_image_url_type(client: openai.AsyncOpenAI,
163
model_name: str,
164
image_url: str):
@@ -187,7 +188,7 @@ async def test_error_on_invalid_image_url_type(client: openai.AsyncOpenAI,
188
189
@pytest.mark.asyncio
190
@pytest.mark.parametrize("model_name", [MODEL_NAME])
190
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
191
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
192
async def test_single_chat_session_image_beamsearch(client: openai.AsyncOpenAI,
193
model_name: str,
194
image_url: str):
@@ -223,10 +224,11 @@ async def test_single_chat_session_image_beamsearch(client: openai.AsyncOpenAI,
224
225
@pytest.mark.asyncio
226
@pytest.mark.parametrize("model_name", [MODEL_NAME])
226
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
227
+@pytest.mark.parametrize("raw_image_url", TEST_IMAGE_ASSETS)
228
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
229
async def test_single_chat_session_image_base64encoded(
228
- client: openai.AsyncOpenAI, model_name: str, image_url: str,
229
- base64_encoded_image: dict[str, str]):
230
+ client: openai.AsyncOpenAI, model_name: str, raw_image_url: str,
231
+ image_url: str, base64_encoded_image: dict[str, str]):
232
233
content_text = "What's in this image?"
234
messages = [{
@@ -237,7 +239,7 @@ async def test_single_chat_session_image_base64encoded(
239
"type": "image_url",
240
"image_url": {
241
"url":
240
- f"data:image/jpeg;base64,{base64_encoded_image[image_url]}"
242
+ f"data:image/jpeg;base64,{base64_encoded_image[raw_image_url]}"
243
}
244
},
245
{
@@ -287,12 +289,12 @@ async def test_single_chat_session_image_base64encoded(
289
290
@pytest.mark.asyncio
291
@pytest.mark.parametrize("model_name", [MODEL_NAME])
290
-@pytest.mark.parametrize("image_idx", list(range(len(TEST_IMAGE_URLS))))
292
+@pytest.mark.parametrize("image_idx", list(range(len(TEST_IMAGE_ASSETS))))
293
async def test_single_chat_session_image_base64encoded_beamsearch(
294
client: openai.AsyncOpenAI, model_name: str, image_idx: int,
295
base64_encoded_image: dict[str, str]):
296
# NOTE: This test also validates that we pass MM data through beam search
295
- image_url = TEST_IMAGE_URLS[image_idx]
297
+ raw_image_url = TEST_IMAGE_ASSETS[image_idx]
298
expected_res = EXPECTED_MM_BEAM_SEARCH_RES[image_idx]
299
300
messages = [{
@@ -303,7 +305,7 @@ async def test_single_chat_session_image_base64encoded_beamsearch(
305
"type": "image_url",
306
"image_url": {
307
"url":
306
- f"data:image/jpeg;base64,{base64_encoded_image[image_url]}"
308
+ f"data:image/jpeg;base64,{base64_encoded_image[raw_image_url]}"
309
}
310
},
311
{
@@ -326,7 +328,7 @@ async def test_single_chat_session_image_base64encoded_beamsearch(
328
329
@pytest.mark.asyncio
330
@pytest.mark.parametrize("model_name", [MODEL_NAME])
329
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
331
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
332
async def test_chat_streaming_image(client: openai.AsyncOpenAI,
333
model_name: str, image_url: str):
334
messages = [{
@@ -385,7 +387,8 @@ async def test_chat_streaming_image(client: openai.AsyncOpenAI,
387
@pytest.mark.parametrize("model_name", [MODEL_NAME])
388
@pytest.mark.parametrize(
389
"image_urls",
388
- [TEST_IMAGE_URLS[:i] for i in range(2, len(TEST_IMAGE_URLS))])
390
+ [TEST_IMAGE_ASSETS[:i] for i in range(2, len(TEST_IMAGE_ASSETS))],
391
+ indirect=True)
392
async def test_multi_image_input(client: openai.AsyncOpenAI, model_name: str,
393
image_urls: list[str]):
394
tests/entrypoints/openai/test_vision_embedding.py
+10
-9
@@ -19,11 +19,11 @@ vlm2vec_jinja_path = VLLM_PATH / "examples/template_vlm2vec.jinja"
19
assert vlm2vec_jinja_path.exists()
20
21
# Test different image extensions (JPG/PNG) and formats (gray/RGB/RGBA)
22
-TEST_IMAGE_URLS = [
23
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
24
- "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
25
- "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
26
- "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
22
+TEST_IMAGE_ASSETS = [
23
+ "2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", # "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
24
+ "Grayscale_8bits_palette_sample_image.png", # "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
25
+ "1280px-Venn_diagram_rgb.svg.png", # "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
26
+ "RGBA_comp.png", # "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
27
]
28
29
@@ -49,10 +49,11 @@ def server():
49
50
51
@pytest.fixture(scope="session")
52
-def base64_encoded_image() -> dict[str, str]:
52
+def base64_encoded_image(local_asset_server) -> dict[str, str]:
53
return {
54
- image_url: encode_image_base64(fetch_image(image_url))
55
- for image_url in TEST_IMAGE_URLS
54
+ image_url:
55
+ encode_image_base64(local_asset_server.get_image_asset(image_url))
56
+ for image_url in TEST_IMAGE_ASSETS
57
}
58
59
@@ -70,7 +71,7 @@ def get_hf_prompt_tokens(model_name, content, image_url):
71
72
@pytest.mark.asyncio
73
@pytest.mark.parametrize("model_name", [MODEL_NAME])
73
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
74
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
75
async def test_image_embedding(server: RemoteOpenAIServer, model_name: str,
76
image_url: str):
77
content_text = "Represent the given image."
tests/models/multimodal/generation/test_pixtral.py
+32
-25
@@ -29,10 +29,10 @@ MISTRAL_SMALL_3_1_ID = "mistralai/Mistral-Small-3.1-24B-Instruct-2503"
29
MODELS = [PIXTRAL_ID, MISTRAL_SMALL_3_1_ID]
30
31
IMG_URLS = [
32
- "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/237-400x300.jpg",
33
- "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/231-200x300.jpg",
34
- "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/27-500x500.jpg",
35
- "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/17-150x600.jpg",
32
+ "237-400x300.jpg", # "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/237-400x300.jpg",
33
+ "231-200x300.jpg", # "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/237-400x300.jpg",
34
+ "27-500x500.jpg", # "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/237-400x300.jpg",
35
+ "17-150x600.jpg", # "https://huggingface.co/datasets/Isotr0py/mistral-test-images/resolve/main/237-400x300.jpg",
36
]
37
PROMPT = "Describe each image in one short sentence."
38
@@ -105,12 +105,6 @@ def _create_engine_inputs_hf(urls: list[str]) -> TextPrompt:
105
return engine_inputs
106
107
108
-MSGS = [
109
- _create_msg_format(IMG_URLS[:1]),
110
- _create_msg_format(IMG_URLS[:2]),
111
- _create_msg_format(IMG_URLS),
112
-]
113
-
108
SAMPLING_PARAMS = SamplingParams(max_tokens=512, temperature=0.0, logprobs=5)
109
LIMIT_MM_PER_PROMPT = dict(image=4)
110
@@ -156,12 +150,8 @@ def load_outputs_w_logprobs(filename: "StrPath") -> OutputsLogprobs:
150
@pytest.mark.parametrize("model", MODELS)
151
@pytest.mark.parametrize("max_model_len", MAX_MODEL_LEN)
152
@pytest.mark.parametrize("dtype", ["bfloat16"])
159
-def test_chat(
160
- vllm_runner,
161
- max_model_len: int,
162
- model: str,
163
- dtype: str,
164
-) -> None:
153
+def test_chat(vllm_runner, max_model_len: int, model: str, dtype: str,
154
+ local_asset_server) -> None:
155
EXPECTED_CHAT_LOGPROBS = load_outputs_w_logprobs(
156
FIXTURE_LOGPROBS_CHAT[model])
157
with vllm_runner(
@@ -174,7 +164,14 @@ def test_chat(
164
limit_mm_per_prompt=LIMIT_MM_PER_PROMPT,
165
) as vllm_model:
166
outputs = []
177
- for msg in MSGS:
167
+
168
+ urls_all = [local_asset_server.url_for(u) for u in IMG_URLS]
169
+ msgs = [
170
+ _create_msg_format(urls_all[:1]),
171
+ _create_msg_format(urls_all[:2]),
172
+ _create_msg_format(urls_all),
173
+ ]
174
+ for msg in msgs:
175
output = vllm_model.llm.chat(msg, sampling_params=SAMPLING_PARAMS)
176
177
outputs.extend(output)
@@ -190,14 +187,24 @@ def test_chat(
187
name_1="output")
188
189
193
-@pytest.mark.parametrize("prompt,expected_ranges",
194
- [(_create_engine_inputs_hf(IMG_URLS[:1]),
195
- [PlaceholderRange(offset=11, length=494)]),
196
- (_create_engine_inputs_hf(IMG_URLS[1:4]), [
197
- PlaceholderRange(offset=11, length=266),
198
- PlaceholderRange(offset=277, length=1056),
199
- PlaceholderRange(offset=1333, length=418)
200
- ])])
190
+@pytest.fixture
191
+def prompt(request, local_asset_server) -> TextPrompt:
192
+ names = request.param
193
+ urls = [local_asset_server.url_for(n) for n in names]
194
+ return _create_engine_inputs_hf(urls)
195
+
196
+
197
+@pytest.mark.parametrize(
198
+ "prompt,expected_ranges",
199
+ [
200
+ pytest.param(IMG_URLS[:1], [PlaceholderRange(offset=11, length=494)]),
201
+ pytest.param(IMG_URLS[1:4], [
202
+ PlaceholderRange(offset=11, length=266),
203
+ PlaceholderRange(offset=277, length=1056),
204
+ PlaceholderRange(offset=1333, length=418)
205
+ ])
206
+ ],
207
+)
208
def test_multi_modal_placeholders(vllm_runner, prompt: TextPrompt,
209
expected_ranges: list[PlaceholderRange],
210
monkeypatch) -> None:
tests/multimodal/test_utils.py
+15
-16
@@ -31,11 +31,11 @@ if TYPE_CHECKING:
31
from vllm.multimodal.inputs import MultiModalPlaceholderDict
32
33
# Test different image extensions (JPG/PNG) and formats (gray/RGB/RGBA)
34
-TEST_IMAGE_URLS = [
35
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
36
- "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
37
- "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
38
- "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
34
+TEST_IMAGE_ASSETS = [
35
+ "2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", # "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
36
+ "Grayscale_8bits_palette_sample_image.png", # "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
37
+ "1280px-Venn_diagram_rgb.svg.png", # "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
38
+ "RGBA_comp.png", # "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
39
]
40
41
TEST_VIDEO_URLS = [
@@ -45,12 +45,11 @@ TEST_VIDEO_URLS = [
45
46
47
@pytest.fixture(scope="module")
48
-def url_images() -> dict[str, Image.Image]:
49
- connector = MediaConnector()
48
+def url_images(local_asset_server) -> dict[str, Image.Image]:
49
50
return {
52
- image_url: connector.fetch_image(image_url)
53
- for image_url in TEST_IMAGE_URLS
51
+ image_url: local_asset_server.get_image_asset(image_url)
52
+ for image_url in TEST_IMAGE_ASSETS
53
}
54
55
@@ -69,7 +68,7 @@ def _image_equals(a: Image.Image, b: Image.Image) -> bool:
68
69
70
@pytest.mark.asyncio
72
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
71
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
72
async def test_fetch_image_http(image_url: str):
73
connector = MediaConnector()
74
@@ -79,12 +78,12 @@ async def test_fetch_image_http(image_url: str):
78
79
80
@pytest.mark.asyncio
82
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
81
+@pytest.mark.parametrize("raw_image_url", TEST_IMAGE_ASSETS)
82
@pytest.mark.parametrize("suffix", get_supported_suffixes())
83
async def test_fetch_image_base64(url_images: dict[str, Image.Image],
85
- image_url: str, suffix: str):
84
+ raw_image_url: str, suffix: str):
85
connector = MediaConnector()
87
- url_image = url_images[image_url]
86
+ url_image = url_images[raw_image_url]
87
88
try:
89
mime_type = Image.MIME[Image.registered_extensions()[suffix]]
@@ -117,7 +116,7 @@ async def test_fetch_image_base64(url_images: dict[str, Image.Image],
116
117
118
@pytest.mark.asyncio
120
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
119
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
120
async def test_fetch_image_local_files(image_url: str):
121
connector = MediaConnector()
122
@@ -152,8 +151,8 @@ async def test_fetch_image_local_files(image_url: str):
151
152
153
@pytest.mark.asyncio
155
-async def test_fetch_image_local_files_with_space_in_name():
156
- image_url = TEST_IMAGE_URLS[0]
154
+@pytest.mark.parametrize("image_url", [TEST_IMAGE_ASSETS[0]], indirect=True)
155
+async def test_fetch_image_local_files_with_space_in_name(image_url: str):
156
connector = MediaConnector()
157
158
with TemporaryDirectory() as temp_dir:
tests/v1/entrypoints/openai/responses/test_image.py
+16
-14
@@ -8,17 +8,17 @@ import pytest
8
import pytest_asyncio
9
10
from tests.utils import RemoteOpenAIServer
11
-from vllm.multimodal.utils import encode_image_base64, fetch_image
11
+from vllm.multimodal.utils import encode_image_base64
12
13
# Use a small vision model for testing
14
MODEL_NAME = "Qwen/Qwen2.5-VL-3B-Instruct"
15
MAXIMUM_IMAGES = 2
16
# Test different image extensions (JPG/PNG) and formats (gray/RGB/RGBA)
17
-TEST_IMAGE_URLS = [
18
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
19
- "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
20
- "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
21
- "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
17
+TEST_IMAGE_ASSETS = [
18
+ "2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", # "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
19
+ "Grayscale_8bits_palette_sample_image.png", # "https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png",
20
+ "1280px-Venn_diagram_rgb.svg.png", # "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Venn_diagram_rgb.svg/1280px-Venn_diagram_rgb.svg.png",
21
+ "RGBA_comp.png", # "https://upload.wikimedia.org/wikipedia/commons/0/0b/RGBA_comp.png",
22
]
23
24
@@ -52,16 +52,17 @@ async def client(image_server):
52
53
54
@pytest.fixture(scope="session")
55
-def base64_encoded_image() -> dict[str, str]:
55
+def base64_encoded_image(local_asset_server) -> dict[str, str]:
56
return {
57
- image_url: encode_image_base64(fetch_image(image_url))
58
- for image_url in TEST_IMAGE_URLS
57
+ image_url:
58
+ encode_image_base64(local_asset_server.get_image_asset(image_url))
59
+ for image_url in TEST_IMAGE_ASSETS
60
}
61
62
63
@pytest.mark.asyncio
64
@pytest.mark.parametrize("model_name", [MODEL_NAME])
64
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
65
+@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
66
async def test_single_chat_session_image(client: openai.AsyncOpenAI,
67
model_name: str, image_url: str):
68
content_text = "What's in this image?"
@@ -91,11 +92,11 @@ async def test_single_chat_session_image(client: openai.AsyncOpenAI,
92
93
@pytest.mark.asyncio
94
@pytest.mark.parametrize("model_name", [MODEL_NAME])
94
-@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
95
+@pytest.mark.parametrize("raw_image_url", TEST_IMAGE_ASSETS)
96
async def test_single_chat_session_image_base64encoded(
97
client: openai.AsyncOpenAI,
98
model_name: str,
98
- image_url: str,
99
+ raw_image_url: str,
100
base64_encoded_image: dict[str, str],
101
):
102
content_text = "What's in this image?"
@@ -106,7 +107,7 @@ async def test_single_chat_session_image_base64encoded(
107
{
108
"type": "input_image",
109
"image_url":
109
- f"data:image/jpeg;base64,{base64_encoded_image[image_url]}",
110
+ f"data:image/jpeg;base64,{base64_encoded_image[raw_image_url]}",
111
"detail": "auto",
112
},
113
{
@@ -127,7 +128,8 @@ async def test_single_chat_session_image_base64encoded(
128
@pytest.mark.parametrize("model_name", [MODEL_NAME])
129
@pytest.mark.parametrize(
130
"image_urls",
130
- [TEST_IMAGE_URLS[:i] for i in range(2, len(TEST_IMAGE_URLS))])
131
+ [TEST_IMAGE_ASSETS[:i] for i in range(2, len(TEST_IMAGE_ASSETS))],
132
+ indirect=True)
133
async def test_multi_image_input(client: openai.AsyncOpenAI, model_name: str,
134
image_urls: list[str]):
135
messages = [{
tests/v1/tpu/test_multimodal.py
+7
-6
@@ -4,18 +4,19 @@
4
import openai
5
import pytest
6
7
-from vllm.multimodal.utils import encode_image_base64, fetch_image
7
+from vllm.multimodal.utils import encode_image_base64
8
from vllm.platforms import current_platform
9
10
-from ...entrypoints.openai.test_vision import TEST_IMAGE_URLS
10
+from ...entrypoints.openai.test_vision import TEST_IMAGE_ASSETS
11
from ...utils import RemoteOpenAIServer
12
13
14
@pytest.fixture(scope="session")
15
-def base64_encoded_image() -> dict[str, str]:
15
+def base64_encoded_image(local_asset_server) -> dict[str, str]:
16
return {
17
- image_url: encode_image_base64(fetch_image(image_url))
18
- for image_url in TEST_IMAGE_URLS
17
+ image_asset:
18
+ encode_image_base64(local_asset_server.get_image_asset(image_asset))
19
+ for image_asset in TEST_IMAGE_ASSETS
20
}
21
22
@@ -66,7 +67,7 @@ async def test_basic_vision(model_name: str, base64_encoded_image: dict[str,
67
client: openai.AsyncOpenAI = remote_server.get_async_client()
68
69
# Other requests now should be much faster
69
- for image_url in TEST_IMAGE_URLS:
70
+ for image_url in TEST_IMAGE_ASSETS:
71
image_base64 = base64_encoded_image[image_url]
72
chat_completion_from_base64 = await client.chat.completions\
73
.create(
vllm/assets/image.py
+22
-6
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from dataclasses import dataclass
5
+from pathlib import Path
6
from typing import Literal
7
8
import torch
@@ -11,17 +12,29 @@ from .base import get_vllm_public_assets
12
13
VLM_IMAGES_DIR = "vision_model_images"
14
14
-ImageAssetName = Literal["stop_sign", "cherry_blossom", "hato"]
15
+ImageAssetName = Literal["stop_sign", "cherry_blossom", "hato",
16
+ "2560px-Gfp-wisconsin-madison-the-nature-boardwalk",
17
+ "Grayscale_8bits_palette_sample_image",
18
+ "1280px-Venn_diagram_rgb", "RGBA_comp", "237-400x300",
19
+ "231-200x300", "27-500x500", "17-150x600",
20
+ "handelsblatt-preview", "paper-11"]
21
22
23
@dataclass(frozen=True)
24
class ImageAsset:
25
name: ImageAssetName
26
27
+ def get_path(self, ext: str) -> Path:
28
+ """
29
+ Return s3 path for given image.
30
+ """
31
+ return get_vllm_public_assets(filename=f"{self.name}.{ext}",
32
+ s3_prefix=VLM_IMAGES_DIR)
33
+
34
@property
22
- def pil_image(self) -> Image.Image:
23
- image_path = get_vllm_public_assets(filename=f"{self.name}.jpg",
24
- s3_prefix=VLM_IMAGES_DIR)
35
+ def pil_image(self, ext="jpg") -> Image.Image:
36
+
37
+ image_path = self.get_path(ext)
38
return Image.open(image_path)
39
40
@property
@@ -29,6 +42,9 @@ class ImageAsset:
42
"""
43
Image embeddings, only used for testing purposes with llava 1.5.
44
"""
32
- image_path = get_vllm_public_assets(filename=f"{self.name}.pt",
33
- s3_prefix=VLM_IMAGES_DIR)
45
+ image_path = self.get_path('pt')
46
return torch.load(image_path, map_location="cpu", weights_only=True)
47
+
48
+ def read_bytes(self, ext: str) -> bytes:
49
+ p = Path(self.get_path(ext))
50
+ return p.read_bytes()