@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 70549c1245
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 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()