[ROCm][CI] Solidify entrypoint LLM lifecycle (#51280)

Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com> Signed-off-by: Andreas Karatzas <akaratza@amd.com>

Andreas Karatzas committed Aug 13, 2026 at 12:24 UTC b96bcd0b474fe469c42423f6a4e7de8ba16baa01
24 files changed +974 -1075
tests/entrypoints/llm/offline_mode/test_offline_mode.py
+16 -10
@@ -9,9 +9,6 @@ import pytest
9 import regex as re
10 import urllib3
11
12 -from vllm import LLM
13 -from vllm.distributed import cleanup_dist_env_and_memory
14 -
12 MODEL_CONFIGS = [
13 {
14 "model": "facebook/opt-125m",
@@ -55,19 +52,26 @@ MODEL_CONFIGS = [
52 ]
53
54
55 +def _create_runner(vllm_runner, model_config):
56 + runner_config = model_config.copy()
57 + model = runner_config.pop("model")
58 + tokenizer_name = runner_config.pop("tokenizer", None)
59 + return vllm_runner(model, tokenizer_name=tokenizer_name, **runner_config)
60 +
61 +
62 @pytest.fixture(scope="module")
59 -def cache_models():
63 +def cache_models(vllm_runner):
64 # Cache model files first
65 for model_config in MODEL_CONFIGS:
62 - LLM(**model_config)
63 - cleanup_dist_env_and_memory()
66 + with _create_runner(vllm_runner, model_config):
67 + pass
68
69 yield
70
71
72 @pytest.mark.skip_global_cleanup
73 @pytest.mark.usefixtures("cache_models")
70 -def test_offline_mode(monkeypatch: pytest.MonkeyPatch):
74 +def test_offline_mode(monkeypatch: pytest.MonkeyPatch, vllm_runner):
75 # Set HF to offline mode and ensure we can still construct an LLM
76 with monkeypatch.context() as m:
77 try:
@@ -93,7 +97,8 @@ def test_offline_mode(monkeypatch: pytest.MonkeyPatch):
97 _re_import_modules()
98 # Cached model files should be used in offline mode
99 for model_config in MODEL_CONFIGS:
96 - LLM(**model_config)
100 + with _create_runner(vllm_runner, model_config):
101 + pass
102 finally:
103 # Reset the environment after the test
104 # NB: Assuming tests are run in online mode
@@ -136,7 +141,7 @@ def _re_import_modules():
141
142 @pytest.mark.skip_global_cleanup
143 @pytest.mark.usefixtures("cache_models")
139 -def test_model_from_huggingface_offline(monkeypatch: pytest.MonkeyPatch):
144 +def test_model_from_huggingface_offline(monkeypatch: pytest.MonkeyPatch, vllm_runner):
145 # Set HF to offline mode and ensure we can still construct an LLM
146 with monkeypatch.context() as m:
147 try:
@@ -159,7 +164,8 @@ def test_model_from_huggingface_offline(monkeypatch: pytest.MonkeyPatch):
164 # Need to re-import huggingface_hub
165 # and friends to set up offline mode
166 _re_import_modules()
162 - LLM(model="facebook/opt-125m")
167 + with vllm_runner("facebook/opt-125m"):
168 + pass
169 finally:
170 # Reset the environment after the test
171 # NB: Assuming tests are run in online mode
tests/entrypoints/llm/test_chat.py
+21 -36
@@ -4,46 +4,36 @@ import weakref
4
5 import pytest
6
7 -from vllm import LLM
8 -from vllm.distributed import cleanup_dist_env_and_memory
7 from vllm.exceptions import VLLMValidationError
8 from vllm.sampling_params import SamplingParams
9
10
11 @pytest.fixture(scope="function")
14 -def text_llm():
15 - # pytest caches the fixture so we use weakref.proxy to
16 - # enable garbage collection
17 - llm = LLM(model="meta-llama/Llama-3.2-1B-Instruct", enforce_eager=True, seed=0)
18 -
19 - yield weakref.proxy(llm)
20 -
21 - del llm
22 -
23 - cleanup_dist_env_and_memory()
12 +def text_llm(vllm_runner):
13 + with vllm_runner(
14 + "meta-llama/Llama-3.2-1B-Instruct", enforce_eager=True, seed=0
15 + ) as runner:
16 + # pytest caches yielded fixtures until after teardown, so use a proxy to
17 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
18 + yield weakref.proxy(runner.llm)
19
20
21 @pytest.fixture(scope="function")
27 -def llm_for_failure_test():
22 +def llm_for_failure_test(vllm_runner):
23 """
24 Fixture for testing issue #26081.
25 Uses a small max_model_len to easily trigger length errors.
26 """
32 - # pytest caches the fixture so we use weakref.proxy to
33 - # enable garbage collection
34 - llm = LLM(
35 - model="meta-llama/Llama-3.2-1B-Instruct",
27 + with vllm_runner(
28 + "meta-llama/Llama-3.2-1B-Instruct",
29 enforce_eager=True,
30 seed=0,
31 max_model_len=128,
32 disable_log_stats=True,
40 - )
41 -
42 - yield weakref.proxy(llm)
43 -
44 - del llm
45 -
46 - cleanup_dist_env_and_memory()
33 + ) as runner:
34 + # pytest caches yielded fixtures until after teardown, so use a proxy to
35 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
36 + yield weakref.proxy(runner.llm)
37
38
39 def test_chat(text_llm):
@@ -99,21 +89,16 @@ def test_llm_chat_tokenization_no_double_bos(text_llm):
89
90
91 @pytest.fixture(scope="function")
102 -def thinking_llm():
103 - # pytest caches the fixture so we use weakref.proxy to
104 - # enable garbage collection
105 - llm = LLM(
106 - model="Qwen/Qwen3-0.6B",
92 +def thinking_llm(vllm_runner):
93 + with vllm_runner(
94 + "Qwen/Qwen3-0.6B",
95 max_model_len=4096,
96 enforce_eager=True,
97 seed=0,
110 - )
111 -
112 - yield weakref.proxy(llm)
113 -
114 - del llm
115 -
116 - cleanup_dist_env_and_memory()
98 + ) as runner:
99 + # pytest caches yielded fixtures until after teardown, so use a proxy to
100 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
101 + yield weakref.proxy(runner.llm)
102
103
104 @pytest.mark.parametrize("enable_thinking", [True, False])
tests/entrypoints/llm/test_collective_rpc.py
+5 -7
@@ -4,15 +4,13 @@
4 import pytest
5 import torch
6
7 -from vllm import LLM
8 -
7 from ...utils import create_new_process_for_each_test
8
9
10 @pytest.mark.parametrize("tp_size", [1, 2])
11 @pytest.mark.parametrize("backend", ["mp", "ray"])
12 @create_new_process_for_each_test()
15 -def test_collective_rpc(tp_size, backend, monkeypatch):
13 +def test_collective_rpc(tp_size, backend, monkeypatch, vllm_runner):
14 if torch.accelerator.device_count() < tp_size:
15 pytest.skip(f"Not enough GPUs for tensor parallelism {tp_size}")
16 if tp_size == 1 and backend == "ray":
@@ -26,11 +24,11 @@ def test_collective_rpc(tp_size, backend, monkeypatch):
24 return self.rank
25
26 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
29 - llm = LLM(
30 - model="hmellor/tiny-random-LlamaForCausalLM",
27 + with vllm_runner(
28 + "hmellor/tiny-random-LlamaForCausalLM",
29 enforce_eager=True,
30 load_format="dummy",
31 tensor_parallel_size=tp_size,
32 distributed_executor_backend=backend,
35 - )
36 - assert llm.collective_rpc(echo_rank) == list(range(tp_size))
33 + ) as runner:
34 + assert runner.llm.collective_rpc(echo_rank) == list(range(tp_size))
tests/entrypoints/llm/test_generate.py
+30 -36
@@ -6,7 +6,6 @@ import weakref
6 import pytest
7
8 from vllm import LLM, SamplingParams
9 -from vllm.distributed import cleanup_dist_env_and_memory
9
10 MODEL_NAME = "distilbert/distilgpt2"
11
@@ -26,22 +25,17 @@ TOKEN_IDS = [
25
26
27 @pytest.fixture(scope="module")
29 -def llm():
30 - # pytest caches the fixture so we use weakref.proxy to
31 - # enable garbage collection
32 - llm = LLM(
33 - model=MODEL_NAME,
28 +def llm(vllm_runner):
29 + with vllm_runner(
30 + MODEL_NAME,
31 max_num_batched_tokens=4096,
32 tensor_parallel_size=1,
33 gpu_memory_utilization=0.10,
34 enforce_eager=True,
38 - )
39 -
40 - yield weakref.proxy(llm)
41 -
42 - del llm
43 -
44 - cleanup_dist_env_and_memory()
35 + ) as runner:
36 + # pytest caches yielded fixtures until after teardown, so use a proxy to
37 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
38 + yield weakref.proxy(runner.llm)
39
40
41 @pytest.mark.skip_global_cleanup
@@ -97,34 +91,34 @@ def test_single_prompt_priority(llm: LLM):
91 assert len(outputs) == 1
92
93
100 -def test_max_model_len():
94 +def test_max_model_len(vllm_runner):
95 max_model_len = 20
102 - llm = LLM(
103 - model=MODEL_NAME,
96 + with vllm_runner(
97 + MODEL_NAME,
98 max_model_len=max_model_len,
99 gpu_memory_utilization=0.10,
100 enforce_eager=True, # reduce test time
107 - )
108 - sampling_params = SamplingParams(max_tokens=max_model_len + 10)
109 - outputs = llm.generate(PROMPTS, sampling_params)
110 - for output in outputs:
111 - num_total_tokens = len(output.prompt_token_ids) + len(
112 - output.outputs[0].token_ids
113 - )
114 - # Total tokens must not exceed max_model_len.
115 - # It can be less if generation finishes due to other reasons (e.g., EOS)
116 - # before reaching the absolute model length limit.
117 - assert num_total_tokens <= max_model_len
118 -
119 -
120 -def test_log_stats():
121 - llm = LLM(
122 - model=MODEL_NAME,
101 + ) as runner:
102 + sampling_params = SamplingParams(max_tokens=max_model_len + 10)
103 + outputs = runner.llm.generate(PROMPTS, sampling_params)
104 + for output in outputs:
105 + num_total_tokens = len(output.prompt_token_ids) + len(
106 + output.outputs[0].token_ids
107 + )
108 + # Total tokens must not exceed max_model_len.
109 + # It can be less if generation finishes due to other reasons (e.g., EOS)
110 + # before reaching the absolute model length limit.
111 + assert num_total_tokens <= max_model_len
112 +
113 +
114 +def test_log_stats(vllm_runner):
115 + with vllm_runner(
116 + MODEL_NAME,
117 disable_log_stats=False,
118 gpu_memory_utilization=0.10,
119 enforce_eager=True, # reduce test time
126 - )
127 - outputs = llm.generate(PROMPTS, sampling_params=None)
120 + ) as runner:
121 + outputs = runner.llm.generate(PROMPTS, sampling_params=None)
122
129 - # disable_log_stats is False, every output should have metrics
130 - assert all(output.metrics is not None for output in outputs)
123 + # disable_log_stats is False, every output should have metrics
124 + assert all(output.metrics is not None for output in outputs)
tests/entrypoints/llm/test_gpu_utilization.py
+25 -12
@@ -1,10 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from vllm import LLM, SamplingParams
4 +from vllm import SamplingParams
5
6
7 -def test_gpu_memory_utilization():
7 +def test_gpu_memory_utilization(vllm_runner):
8 prompts = [
9 "Hello, my name is",
10 "The president of the United States is",
@@ -15,13 +15,26 @@ def test_gpu_memory_utilization():
15
16 # makes sure gpu_memory_utilization is per-instance limit,
17 # not a global limit
18 - llms = [
19 - LLM(model="facebook/opt-125m", gpu_memory_utilization=0.3, enforce_eager=True)
20 - for i in range(3)
21 - ]
22 - for llm in llms:
23 - outputs = llm.generate(prompts, sampling_params)
24 - for output in outputs:
25 - prompt = output.prompt
26 - generated_text = output.outputs[0].text
27 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
18 + with (
19 + vllm_runner(
20 + "facebook/opt-125m",
21 + gpu_memory_utilization=0.3,
22 + enforce_eager=True,
23 + ) as runner_0,
24 + vllm_runner(
25 + "facebook/opt-125m",
26 + gpu_memory_utilization=0.3,
27 + enforce_eager=True,
28 + ) as runner_1,
29 + vllm_runner(
30 + "facebook/opt-125m",
31 + gpu_memory_utilization=0.3,
32 + enforce_eager=True,
33 + ) as runner_2,
34 + ):
35 + for runner in (runner_0, runner_1, runner_2):
36 + outputs = runner.llm.generate(prompts, sampling_params)
37 + for output in outputs:
38 + prompt = output.prompt
39 + generated_text = output.outputs[0].text
40 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
tests/entrypoints/llm/test_prompt_validation.py
+26 -21
@@ -4,30 +4,35 @@
4 import pytest
5 import torch
6
7 -from vllm import LLM
7 from vllm.exceptions import VLLMValidationError
8
9
11 -def test_empty_prompt():
12 - llm = LLM(model="openai-community/gpt2", enforce_eager=True)
13 - with pytest.raises(VLLMValidationError, match="decoder prompt cannot be empty"):
14 - llm.generate([""])
15 -
16 -
17 -def test_out_of_vocab_token():
18 - llm = LLM(model="openai-community/gpt2", enforce_eager=True)
19 - with pytest.raises(VLLMValidationError, match="out of vocabulary"):
20 - llm.generate({"prompt_token_ids": [999999]})
21 -
22 -
23 -def test_require_mm_embeds():
24 - llm = LLM(
25 - model="llava-hf/llava-1.5-7b-hf",
26 - enforce_eager=True,
27 - enable_mm_embeds=False,
28 - )
29 - with pytest.raises(ValueError, match="--enable-mm-embeds"):
30 - llm.generate(
10 +def test_empty_prompt(vllm_runner):
11 + with (
12 + vllm_runner("openai-community/gpt2", enforce_eager=True) as runner,
13 + pytest.raises(VLLMValidationError, match="decoder prompt cannot be empty"),
14 + ):
15 + runner.llm.generate([""])
16 +
17 +
18 +def test_out_of_vocab_token(vllm_runner):
19 + with (
20 + vllm_runner("openai-community/gpt2", enforce_eager=True) as runner,
21 + pytest.raises(VLLMValidationError, match="out of vocabulary"),
22 + ):
23 + runner.llm.generate({"prompt_token_ids": [999999]})
24 +
25 +
26 +def test_require_mm_embeds(vllm_runner):
27 + with (
28 + vllm_runner(
29 + "llava-hf/llava-1.5-7b-hf",
30 + enforce_eager=True,
31 + enable_mm_embeds=False,
32 + ) as runner,
33 + pytest.raises(ValueError, match="--enable-mm-embeds"),
34 + ):
35 + runner.llm.generate(
36 {
37 "prompt": "<image>",
38 "multi_modal_data": {"image": torch.empty(1, 1, 1)},
tests/entrypoints/llm/test_struct_output_generate.py
+566 -562
@@ -9,13 +9,10 @@ from typing import Any
9 import jsonschema
10 import pytest
11 import regex as re
12 -import torch
12 from pydantic import BaseModel
13
14 from tests.reasoning.utils import run_reasoning_extraction
15 from vllm.config import StructuredOutputsConfig
17 -from vllm.distributed import cleanup_dist_env_and_memory
18 -from vllm.entrypoints.llm import LLM
16 from vllm.outputs import RequestOutput
17 from vllm.platforms import current_platform
18 from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager
@@ -212,11 +209,11 @@ class CarDescription(BaseModel):
209 PARAMS_MODELS_BACKENDS_TOKENIZER_MODE,
210 )
211 def test_structured_output(
215 - request: pytest.FixtureRequest,
212 backend: str,
213 tokenizer_mode: str,
214 model_name: str,
215 speculative_config: dict[str, Any],
216 + vllm_runner,
217 ):
218 sample_json_schema = SAMPLE_JSON_SCHEMA
219 unsupported_json_schema = UNSUPPORTED_JSON_SCHEMA
@@ -229,8 +226,8 @@ def test_structured_output(
226
227 # Use a single LLM instance for several scenarios to
228 # speed up the test suite.
232 - llm = LLM(
233 - model=model_name,
229 + with vllm_runner(
230 + model_name,
231 enforce_eager=True,
232 max_model_len=1024,
233 structured_outputs_config=dict(
@@ -242,181 +239,305 @@ def test_structured_output(
239 config_format="auto" if not model_name.startswith("mistralai/") else "hf",
240 speculative_config=speculative_config,
241 **platform_args,
245 - )
246 - request.addfinalizer(llm.llm_engine.engine_core.shutdown)
247 -
248 - #
249 - # Test 1: Generate JSON output based on a provided schema
250 - #
251 - sampling_params = SamplingParams(
252 - temperature=1.0,
253 - max_tokens=4096,
254 - structured_outputs=StructuredOutputsParams(json=sample_json_schema),
255 - )
256 -
257 - prompt = (
258 - "Give an example JSON for an employee profile that fits this "
259 - "schema. Make the response as short as possible. Schema: "
260 - f"{sample_json_schema}"
261 - )
262 - outputs = llm.generate(
263 - [prompt] * 2,
264 - sampling_params=sampling_params,
265 - use_tqdm=True,
266 - )
267 -
268 - assert outputs is not None
269 -
270 - for output in outputs:
271 - assert output is not None
272 - assert isinstance(output, RequestOutput)
273 - prompt = output.prompt
274 -
275 - generated_text = output.outputs[0].text
276 - assert generated_text is not None
277 - if backend != "lm-format-enforcer":
278 - assert "\n" not in generated_text
279 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
280 - try:
281 - output_json = json.loads(generated_text)
282 - except json.JSONDecodeError as e:
283 - pytest.fail(
284 - f"Invalid JSON from backend={backend}: {generated_text!r}\n"
285 - f"Schema: {sample_json_schema}\nError: {e}"
286 - )
287 - jsonschema.validate(instance=output_json, schema=sample_json_schema)
288 -
289 - #
290 - # Test 2: Generate JSON object without a schema
291 - #
292 - if backend != "outlines":
242 + ) as runner:
243 + #
244 + # Test 1: Generate JSON output based on a provided schema
245 + #
246 sampling_params = SamplingParams(
247 temperature=1.0,
248 max_tokens=4096,
296 - n=2,
297 - structured_outputs=StructuredOutputsParams(json_object=True),
249 + structured_outputs=StructuredOutputsParams(json=sample_json_schema),
250 )
251
300 - outputs = llm.generate(
301 - prompts=(
302 - "Generate a JSON object with curly braces for a person with "
303 - "name and age fields for John Smith who is 31 years old. "
304 - "Make the response as short as possible."
305 - ),
252 + prompt = (
253 + "Give an example JSON for an employee profile that fits this "
254 + "schema. Make the response as short as possible. Schema: "
255 + f"{sample_json_schema}"
256 + )
257 + outputs = runner.llm.generate(
258 + [prompt] * 2,
259 sampling_params=sampling_params,
260 use_tqdm=True,
261 )
262
263 assert outputs is not None
264 +
265 for output in outputs:
266 assert output is not None
267 assert isinstance(output, RequestOutput)
268 + prompt = output.prompt
269
315 - for i in range(2):
316 - generated_text = output.outputs[i].text
317 - print(generated_text)
318 - assert generated_text is not None
270 + generated_text = output.outputs[0].text
271 + assert generated_text is not None
272 + if backend != "lm-format-enforcer":
273 + assert "\n" not in generated_text
274 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
275 + try:
276 + output_json = json.loads(generated_text)
277 + except json.JSONDecodeError as e:
278 + pytest.fail(
279 + f"Invalid JSON from backend={backend}: {generated_text!r}\n"
280 + f"Schema: {sample_json_schema}\nError: {e}"
281 + )
282 + jsonschema.validate(instance=output_json, schema=sample_json_schema)
283
320 - # Parse to verify it is a valid JSON object
321 - parsed_json = json.loads(generated_text)
322 - assert isinstance(parsed_json, dict)
284 + #
285 + # Test 2: Generate JSON object without a schema
286 + #
287 + if backend != "outlines":
288 + sampling_params = SamplingParams(
289 + temperature=1.0,
290 + max_tokens=4096,
291 + n=2,
292 + structured_outputs=StructuredOutputsParams(json_object=True),
293 + )
294 +
295 + outputs = runner.llm.generate(
296 + prompts=(
297 + "Generate a JSON object with curly braces for a person with "
298 + "name and age fields for John Smith who is 31 years old. "
299 + "Make the response as short as possible."
300 + ),
301 + sampling_params=sampling_params,
302 + use_tqdm=True,
303 + )
304 +
305 + assert outputs is not None
306 + for output in outputs:
307 + assert output is not None
308 + assert isinstance(output, RequestOutput)
309 +
310 + for i in range(2):
311 + generated_text = output.outputs[i].text
312 + print(generated_text)
313 + assert generated_text is not None
314
324 - #
325 - # Test 3: test a jsonschema incompatible with xgrammar
326 - #
327 - sampling_params = SamplingParams(
328 - temperature=1.0,
329 - max_tokens=4096,
330 - structured_outputs=StructuredOutputsParams(json=unsupported_json_schema),
331 - )
332 - if backend.startswith("xgrammar"):
333 - with pytest.raises(
334 - ValueError,
335 - match="The provided JSON schema contains features "
336 - "not supported by xgrammar.",
337 - ):
315 + # Parse to verify it is a valid JSON object
316 + parsed_json = json.loads(generated_text)
317 + assert isinstance(parsed_json, dict)
318 +
319 + #
320 + # Test 3: test a jsonschema incompatible with xgrammar
321 + #
322 + sampling_params = SamplingParams(
323 + temperature=1.0,
324 + max_tokens=4096,
325 + structured_outputs=StructuredOutputsParams(json=unsupported_json_schema),
326 + )
327 + if backend.startswith("xgrammar"):
328 + with pytest.raises(
329 + ValueError,
330 + match="The provided JSON schema contains features "
331 + "not supported by xgrammar.",
332 + ):
333 + prompt = (
334 + f"Give an example JSON for an employee profile that "
335 + f"fits this schema: {unsupported_json_schema}. "
336 + f"Make the response as short as possible."
337 + )
338 + runner.llm.generate(
339 + [prompt] * 2,
340 + sampling_params=sampling_params,
341 + use_tqdm=True,
342 + )
343 + else:
344 prompt = (
339 - f"Give an example JSON for an employee profile that "
345 + f"Give an example JSON object for a grade that "
346 f"fits this schema: {unsupported_json_schema}. "
347 f"Make the response as short as possible."
348 )
343 - llm.generate(
344 - [prompt] * 2,
349 + outputs = runner.llm.generate(
350 + prompt,
351 + sampling_params=sampling_params,
352 + use_tqdm=True,
353 + )
354 + assert outputs is not None
355 + for output in outputs:
356 + assert output is not None
357 + assert isinstance(output, RequestOutput)
358 + generated_text = output.outputs[0].text
359 + assert generated_text is not None
360 + print(generated_text)
361 +
362 + # Parse to verify it is valid JSON
363 + parsed_json = json.loads(generated_text)
364 + assert isinstance(parsed_json, dict)
365 +
366 + if backend not in ["outlines", "lm-format-enforcer"]:
367 + #
368 + # Test 4: Generate SQL statement using EBNF grammar
369 + #
370 + sampling_params = SamplingParams(
371 + temperature=0.8,
372 + top_p=0.95,
373 + max_tokens=1000,
374 + structured_outputs=StructuredOutputsParams(grammar=sample_sql_ebnf),
375 + )
376 + outputs = runner.llm.generate(
377 + (
378 + "Generate a sql statement that selects col_1 from "
379 + "table_1 where it is equal to 1. Make the response as short as "
380 + "possible."
381 + ),
382 + sampling_params=sampling_params,
383 + use_tqdm=True,
384 + )
385 +
386 + assert outputs is not None
387 + for output in outputs:
388 + assert output is not None
389 + assert isinstance(output, RequestOutput)
390 + prompt = output.prompt
391 +
392 + generated_text = output.outputs[0].text
393 + assert generated_text is not None
394 +
395 + # remove spaces for comparison b/c we removed them in the grammar
396 + ground_truth = "SELECT col_1 from table_1 where col_1 = 1".replace(
397 + " ", ""
398 + )
399 +
400 + assert generated_text.strip() == ground_truth
401 +
402 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
403 +
404 + #
405 + # Test 5: Generate SQL statement using Lark grammar
406 + #
407 + sampling_params = SamplingParams(
408 + temperature=0.8,
409 + top_p=0.95,
410 + max_tokens=1000,
411 + structured_outputs=StructuredOutputsParams(grammar=sample_sql_lark),
412 + )
413 + outputs = runner.llm.generate(
414 + (
415 + "Generate a sql statement that selects col_1 from "
416 + "table_1 where it is equal to 1. Make the response as short as "
417 + "possible."
418 + ),
419 sampling_params=sampling_params,
420 use_tqdm=True,
421 )
348 - else:
422 +
423 + assert outputs is not None
424 + for output in outputs:
425 + assert output is not None
426 + assert isinstance(output, RequestOutput)
427 + prompt = output.prompt
428 +
429 + generated_text = output.outputs[0].text
430 + assert generated_text is not None
431 +
432 + # use Lark to parse the output, and make sure it's a valid parse tree
433 + from lark import Lark
434 +
435 + parser = Lark(sample_sql_lark)
436 + parser.parse(generated_text)
437 +
438 + # remove spaces for comparison b/c we removed them in the grammar
439 + ground_truth = "SELECT col_1 from table_1 where col_1 = 1".replace(
440 + " ", ""
441 + )
442 +
443 + assert generated_text.strip() == ground_truth
444 +
445 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
446 +
447 + #
448 + # Test 6: Test invalid grammar input
449 + #
450 + sampling_params = SamplingParams(
451 + temperature=0.8,
452 + top_p=0.95,
453 + max_tokens=1000,
454 + structured_outputs=StructuredOutputsParams(grammar="not a grammar"),
455 + )
456 + with pytest.raises(ValueError, match="Failed to convert the grammar "):
457 + runner.llm.generate(
458 + (
459 + "Generate a sql statement that selects col_1 from "
460 + "table_1 where it is equal to 1. Make the response as short "
461 + "as possible."
462 + ),
463 + sampling_params=sampling_params,
464 + use_tqdm=True,
465 + )
466 +
467 + #
468 + # Test 7: Generate text based on a regex pattern
469 + #
470 + sampling_params = SamplingParams(
471 + temperature=0.8,
472 + top_p=0.95,
473 + structured_outputs=StructuredOutputsParams(regex=sample_regex),
474 + )
475 +
476 prompt = (
350 - f"Give an example JSON object for a grade that "
351 - f"fits this schema: {unsupported_json_schema}. "
477 + f"Give an example IPv4 address with this regex: {sample_regex}. "
478 f"Make the response as short as possible."
479 )
354 - outputs = llm.generate(
355 - prompt,
480 + outputs = runner.llm.generate(
481 + [prompt] * 2,
482 sampling_params=sampling_params,
483 use_tqdm=True,
484 )
485 +
486 assert outputs is not None
487 for output in outputs:
488 assert output is not None
489 assert isinstance(output, RequestOutput)
490 + prompt = output.prompt
491 generated_text = output.outputs[0].text
364 - assert generated_text is not None
492 print(generated_text)
493 + assert generated_text is not None
494 + assert re.fullmatch(sample_regex, generated_text) is not None
495 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
496
367 - # Parse to verify it is valid JSON
368 - parsed_json = json.loads(generated_text)
369 - assert isinstance(parsed_json, dict)
370 -
371 - if backend not in ["outlines", "lm-format-enforcer"]:
497 #
373 - # Test 4: Generate SQL statement using EBNF grammar
498 + # Test 8: Generate text based on a choices
499 #
500 sampling_params = SamplingParams(
501 temperature=0.8,
502 top_p=0.95,
378 - max_tokens=1000,
379 - structured_outputs=StructuredOutputsParams(grammar=sample_sql_ebnf),
503 + structured_outputs=StructuredOutputsParams(
504 + choice=sample_structured_outputs_choices
505 + ),
506 )
381 - outputs = llm.generate(
507 +
508 + outputs = runner.llm.generate(
509 (
383 - "Generate a sql statement that selects col_1 from "
384 - "table_1 where it is equal to 1. Make the response as short as "
385 - "possible."
510 + "The best language for type-safe systems programming is "
511 + "(Make the response as short as possible.) "
512 ),
513 sampling_params=sampling_params,
514 use_tqdm=True,
515 )
390 -
516 assert outputs is not None
517 for output in outputs:
518 assert output is not None
519 assert isinstance(output, RequestOutput)
520 prompt = output.prompt
396 -
521 generated_text = output.outputs[0].text
522 + print(generated_text)
523 assert generated_text is not None
399 -
400 - # remove spaces for comparison b/c we removed them in the grammar
401 - ground_truth = "SELECT col_1 from table_1 where col_1 = 1".replace(" ", "")
402 -
403 - assert generated_text.strip() == ground_truth
404 -
524 + assert generated_text in sample_structured_outputs_choices
525 print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
526
527 #
408 - # Test 5: Generate SQL statement using Lark grammar
528 + # Test 9: Generate structured output using a Pydantic model with an enum
529 #
530 + json_schema = CarDescription.model_json_schema()
531 sampling_params = SamplingParams(
411 - temperature=0.8,
412 - top_p=0.95,
532 + temperature=1.0,
533 max_tokens=1000,
414 - structured_outputs=StructuredOutputsParams(grammar=sample_sql_lark),
534 + structured_outputs=StructuredOutputsParams(json=json_schema),
535 )
416 - outputs = llm.generate(
536 +
537 + outputs = runner.llm.generate(
538 (
418 - "Generate a sql statement that selects col_1 from "
419 - "table_1 where it is equal to 1. Make the response as short as "
539 + "Generate a JSON with the brand, model and car_type of the most "
540 + "iconic car from the 90's. Make the response as short as "
541 "possible."
542 ),
543 sampling_params=sampling_params,
@@ -424,6 +545,7 @@ def test_structured_output(
545 )
546
547 assert outputs is not None
548 +
549 for output in outputs:
550 assert output is not None
551 assert isinstance(output, RequestOutput)
@@ -431,220 +553,97 @@ def test_structured_output(
553
554 generated_text = output.outputs[0].text
555 assert generated_text is not None
434 -
435 - # use Lark to parse the output, and make sure it's a valid parse tree
436 - from lark import Lark
437 -
438 - parser = Lark(sample_sql_lark)
439 - parser.parse(generated_text)
440 -
441 - # remove spaces for comparison b/c we removed them in the grammar
442 - ground_truth = "SELECT col_1 from table_1 where col_1 = 1".replace(" ", "")
443 -
444 - assert generated_text.strip() == ground_truth
445 -
556 print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
557 + try:
558 + output_json = json.loads(generated_text)
559 + except json.JSONDecodeError as e:
560 + pytest.fail(
561 + f"Invalid JSON from backend={backend}: {generated_text!r}\n"
562 + f"Schema: {json_schema}\nError: {e}"
563 + )
564 + jsonschema.validate(instance=output_json, schema=json_schema)
565
566 #
449 - # Test 6: Test invalid grammar input
567 + # Test 10: Generate structured with minLength and maxLength
568 #
569 + min_length = 50
570 + max_length = 50
571 + json_schema = {
572 + "type": "object",
573 + "properties": {
574 + "description": {
575 + "type": "string",
576 + "maxLength": max_length,
577 + "minLength": min_length,
578 + }
579 + },
580 + "required": ["description"],
581 + "additionalProperties": False,
582 + }
583 +
584 sampling_params = SamplingParams(
452 - temperature=0.8,
453 - top_p=0.95,
454 - max_tokens=1000,
455 - structured_outputs=StructuredOutputsParams(grammar="not a grammar"),
585 + temperature=1.0,
586 + max_tokens=4096,
587 + structured_outputs=StructuredOutputsParams(json=json_schema),
588 )
457 - with pytest.raises(ValueError, match="Failed to convert the grammar "):
458 - llm.generate(
459 - (
460 - "Generate a sql statement that selects col_1 from "
461 - "table_1 where it is equal to 1. Make the response as short "
462 - "as possible."
463 - ),
464 - sampling_params=sampling_params,
465 - use_tqdm=True,
466 - )
467 -
468 - #
469 - # Test 7: Generate text based on a regex pattern
470 - #
471 - sampling_params = SamplingParams(
472 - temperature=0.8,
473 - top_p=0.95,
474 - structured_outputs=StructuredOutputsParams(regex=sample_regex),
475 - )
476 -
477 - prompt = (
478 - f"Give an example IPv4 address with this regex: {sample_regex}. "
479 - f"Make the response as short as possible."
480 - )
481 - outputs = llm.generate(
482 - [prompt] * 2,
483 - sampling_params=sampling_params,
484 - use_tqdm=True,
485 - )
486 -
487 - assert outputs is not None
488 - for output in outputs:
489 - assert output is not None
490 - assert isinstance(output, RequestOutput)
491 - prompt = output.prompt
492 - generated_text = output.outputs[0].text
493 - print(generated_text)
494 - assert generated_text is not None
495 - assert re.fullmatch(sample_regex, generated_text) is not None
496 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
497 -
498 - #
499 - # Test 8: Generate text based on a choices
500 - #
501 - sampling_params = SamplingParams(
502 - temperature=0.8,
503 - top_p=0.95,
504 - structured_outputs=StructuredOutputsParams(
505 - choice=sample_structured_outputs_choices
506 - ),
507 - )
589
509 - outputs = llm.generate(
510 - (
511 - "The best language for type-safe systems programming is "
512 - "(Make the response as short as possible.) "
513 - ),
514 - sampling_params=sampling_params,
515 - use_tqdm=True,
516 - )
517 - assert outputs is not None
518 - for output in outputs:
519 - assert output is not None
520 - assert isinstance(output, RequestOutput)
521 - prompt = output.prompt
522 - generated_text = output.outputs[0].text
523 - print(generated_text)
524 - assert generated_text is not None
525 - assert generated_text in sample_structured_outputs_choices
526 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
527 -
528 - #
529 - # Test 9: Generate structured output using a Pydantic model with an enum
530 - #
531 - json_schema = CarDescription.model_json_schema()
532 - sampling_params = SamplingParams(
533 - temperature=1.0,
534 - max_tokens=1000,
535 - structured_outputs=StructuredOutputsParams(json=json_schema),
536 - )
537 -
538 - outputs = llm.generate(
539 - (
540 - "Generate a JSON with the brand, model and car_type of the most "
541 - "iconic car from the 90's. Make the response as short as "
542 - "possible."
543 - ),
544 - sampling_params=sampling_params,
545 - use_tqdm=True,
546 - )
590 + outputs = runner.llm.generate(
591 + (
592 + "Generate a description of a frog using 50 characters. "
593 + "Make the response as short as possible."
594 + ),
595 + sampling_params=sampling_params,
596 + use_tqdm=True,
597 + )
598
548 - assert outputs is not None
599 + assert outputs is not None
600
550 - for output in outputs:
551 - assert output is not None
552 - assert isinstance(output, RequestOutput)
553 - prompt = output.prompt
601 + for output in outputs:
602 + assert output is not None
603 + assert isinstance(output, RequestOutput)
604 + prompt = output.prompt
605
555 - generated_text = output.outputs[0].text
556 - assert generated_text is not None
557 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
558 - try:
559 - output_json = json.loads(generated_text)
560 - except json.JSONDecodeError as e:
561 - pytest.fail(
562 - f"Invalid JSON from backend={backend}: {generated_text!r}\n"
563 - f"Schema: {json_schema}\nError: {e}"
564 - )
565 - jsonschema.validate(instance=output_json, schema=json_schema)
566 -
567 - #
568 - # Test 10: Generate structured with minLength and maxLength
569 - #
570 - min_length = 50
571 - max_length = 50
572 - json_schema = {
573 - "type": "object",
574 - "properties": {
575 - "description": {
576 - "type": "string",
577 - "maxLength": max_length,
578 - "minLength": min_length,
606 + generated_text = output.outputs[0].text
607 + assert generated_text is not None
608 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
609 + try:
610 + output_json = json.loads(generated_text)
611 + except json.JSONDecodeError as e:
612 + pytest.fail(
613 + f"Invalid JSON from backend={backend}: {generated_text!r}\n"
614 + f"Schema: {json_schema}\nError: {e}"
615 + )
616 + jsonschema.validate(instance=output_json, schema=json_schema)
617 +
618 + if backend not in ["outlines", "lm-format-enforcer"]:
619 + #
620 + # Test 11: Generate structured output using structural_tag format
621 + #
622 + structural_tag_config = {
623 + "type": "structural_tag",
624 + "structures": [
625 + {
626 + "begin": "<function=get_weather>",
627 + "schema": {
628 + "type": "object",
629 + "properties": {"city": {"type": "string"}},
630 + "additionalProperties": False,
631 + },
632 + "end": "</function>",
633 + }
634 + ],
635 + "triggers": ["<function="],
636 }
580 - },
581 - "required": ["description"],
582 - "additionalProperties": False,
583 - }
584 -
585 - sampling_params = SamplingParams(
586 - temperature=1.0,
587 - max_tokens=4096,
588 - structured_outputs=StructuredOutputsParams(json=json_schema),
589 - )
590 -
591 - outputs = llm.generate(
592 - (
593 - "Generate a description of a frog using 50 characters. "
594 - "Make the response as short as possible."
595 - ),
596 - sampling_params=sampling_params,
597 - use_tqdm=True,
598 - )
599 -
600 - assert outputs is not None
637
602 - for output in outputs:
603 - assert output is not None
604 - assert isinstance(output, RequestOutput)
605 - prompt = output.prompt
606 -
607 - generated_text = output.outputs[0].text
608 - assert generated_text is not None
609 - print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
610 - try:
611 - output_json = json.loads(generated_text)
612 - except json.JSONDecodeError as e:
613 - pytest.fail(
614 - f"Invalid JSON from backend={backend}: {generated_text!r}\n"
615 - f"Schema: {json_schema}\nError: {e}"
638 + sampling_params = SamplingParams(
639 + temperature=0.0,
640 + max_tokens=4096,
641 + structured_outputs=StructuredOutputsParams(
642 + structural_tag=json.dumps(structural_tag_config)
643 + ),
644 )
617 - jsonschema.validate(instance=output_json, schema=json_schema)
618 -
619 - if backend not in ["outlines", "lm-format-enforcer"]:
620 - #
621 - # Test 11: Generate structured output using structural_tag format
622 - #
623 - structural_tag_config = {
624 - "type": "structural_tag",
625 - "structures": [
626 - {
627 - "begin": "<function=get_weather>",
628 - "schema": {
629 - "type": "object",
630 - "properties": {"city": {"type": "string"}},
631 - "additionalProperties": False,
632 - },
633 - "end": "</function>",
634 - }
635 - ],
636 - "triggers": ["<function="],
637 - }
638 -
639 - sampling_params = SamplingParams(
640 - temperature=0.0,
641 - max_tokens=4096,
642 - structured_outputs=StructuredOutputsParams(
643 - structural_tag=json.dumps(structural_tag_config)
644 - ),
645 - )
645
647 - prompt = """
646 + prompt = """
647 You have access to the following function to retrieve the weather in a city:
648
649 {
@@ -683,37 +682,39 @@ Given the previous instructions, what is the weather in New York City? \
682 Make the response as short as possible.
683 """
684
686 - # Change this once other backends support structural_tag
687 - outputs = llm.generate(prompt, sampling_params=sampling_params, use_tqdm=True)
688 - assert outputs is not None
689 -
690 - for output in outputs:
691 - assert output is not None
692 - assert isinstance(output, RequestOutput)
693 - generated_text = output.outputs[0].text
694 - assert generated_text is not None
695 -
696 - # Search for function call pattern in the response
697 - function_call_pattern = r"<function=get_weather>(.*?)</function>"
698 - matches = re.findall(function_call_pattern, generated_text)
685 + # Change this once other backends support structural_tag
686 + outputs = runner.llm.generate(
687 + prompt, sampling_params=sampling_params, use_tqdm=True
688 + )
689 + assert outputs is not None
690
700 - if not matches:
701 - print(
702 - f"Warning: No function calls found in response: {generated_text!r}"
703 - )
704 - continue
691 + for output in outputs:
692 + assert output is not None
693 + assert isinstance(output, RequestOutput)
694 + generated_text = output.outputs[0].text
695 + assert generated_text is not None
696
706 - # Take the first function call if multiple are found
707 - json_str = matches[0]
708 - try:
709 - json_content = json.loads(json_str)
710 - assert "city" in json_content
711 - assert isinstance(json_content["city"], str)
712 - print(f"Found valid function call: {generated_text!r}")
713 - except (json.JSONDecodeError, AssertionError) as e:
714 - pytest.fail(
715 - f"Invalid function call format: {generated_text!r}\nError: {str(e)}"
716 - )
697 + # Search for function call pattern in the response
698 + function_call_pattern = r"<function=get_weather>(.*?)</function>"
699 + matches = re.findall(function_call_pattern, generated_text)
700 +
701 + if not matches:
702 + print(
703 + f"Warning: No function calls found in response: {generated_text!r}"
704 + )
705 + continue
706 +
707 + # Take the first function call if multiple are found
708 + json_str = matches[0]
709 + try:
710 + json_content = json.loads(json_str)
711 + assert "city" in json_content
712 + assert isinstance(json_content["city"], str)
713 + print(f"Found valid function call: {generated_text!r}")
714 + except (json.JSONDecodeError, AssertionError) as e:
715 + pytest.fail(
716 + f"Invalid function call format: {generated_text!r}\nError: {str(e)}"
717 + )
718
719
720 @pytest.mark.parametrize(
@@ -738,14 +739,15 @@ def test_structured_output_with_reasoning_matrices(
739 model_name: str,
740 speculative_config: dict[str, Any] | None,
741 async_scheduling: bool,
742 + vllm_runner,
743 ):
744 if current_platform.is_tpu() and speculative_config:
745 pytest.skip("TPU does not support speculative decoding")
746
747 # Use a single LLM instance for several scenarios to
748 # speed up the test suite.
747 - llm = LLM(
748 - model=model_name,
749 + with vllm_runner(
750 + model_name,
751 # Don't use eager execution on TPUs because we want to test for no
752 # recompilation at runtime
753 enforce_eager=bool(not current_platform.is_tpu()),
@@ -759,271 +761,273 @@ def test_structured_output_with_reasoning_matrices(
761 tokenizer_mode=tokenizer_mode,
762 speculative_config=speculative_config,
763 async_scheduling=async_scheduling,
762 - )
763 - tokenizer = llm.get_tokenizer()
764 - reasoner = ReasoningParserManager.get_reasoning_parser(reasoning_parser)(
765 - tokenizer=tokenizer
766 - )
767 -
768 - reasoning_prompt = "Solve the following math problem step-by-step, then provide the final answer as JSON object with a single key 'result'. Make sure to correct your reasoning if there are any issue should it arise.\nProblem: What is 5 * 8 + 2?" # noqa: E501
769 - reasoning_schema = {
770 - "type": "object",
771 - "properties": {"result": {"type": "integer"}},
772 - "required": ["result"],
773 - "additionalProperties": False,
774 - }
775 - if "Qwen3" in model_name:
776 - reasoning_prompt += "<think>\n"
777 -
778 - sampling_params = SamplingParams(
779 - temperature=0.1,
780 - max_tokens=8192,
781 - structured_outputs=StructuredOutputsParams(json=reasoning_schema),
782 - )
783 - outputs = llm.generate(
784 - [reasoning_prompt],
785 - sampling_params=sampling_params,
786 - use_tqdm=True,
787 - )
788 -
789 - assert outputs is not None
790 - output = outputs[0]
791 - assert output is not None and isinstance(output, RequestOutput)
792 - prompt = output.prompt
793 - generated_text = output.outputs[0].text
794 - reasoning, content = run_reasoning_extraction(reasoner, [generated_text])
795 - print(f"Prompt: {prompt!r}\nReasoning: {reasoning!r}\nContent: {content!r}")
796 -
797 - if "Qwen3" in model_name:
798 - assert content is not None
799 -
800 - assert reasoning is not None
801 -
802 - if content is not None:
803 - output_json = json.loads(content)
804 - jsonschema.validate(instance=output_json, schema=reasoning_schema)
764 + ) as runner:
765 + tokenizer = runner.llm.get_tokenizer()
766 + reasoner = ReasoningParserManager.get_reasoning_parser(reasoning_parser)(
767 + tokenizer=tokenizer
768 + )
769 +
770 + reasoning_prompt = "Solve the following math problem step-by-step, then provide the final answer as JSON object with a single key 'result'. Make sure to correct your reasoning if there are any issue should it arise.\nProblem: What is 5 * 8 + 2?" # noqa: E501
771 + reasoning_schema = {
772 + "type": "object",
773 + "properties": {"result": {"type": "integer"}},
774 + "required": ["result"],
775 + "additionalProperties": False,
776 + }
777 + if "Qwen3" in model_name:
778 + reasoning_prompt += "<think>\n"
779 +
780 + sampling_params = SamplingParams(
781 + temperature=0.1,
782 + max_tokens=8192,
783 + structured_outputs=StructuredOutputsParams(json=reasoning_schema),
784 + )
785 + outputs = runner.llm.generate(
786 + [reasoning_prompt],
787 + sampling_params=sampling_params,
788 + use_tqdm=True,
789 + )
790 +
791 + assert outputs is not None
792 + output = outputs[0]
793 + assert output is not None and isinstance(output, RequestOutput)
794 + prompt = output.prompt
795 + generated_text = output.outputs[0].text
796 + reasoning, content = run_reasoning_extraction(reasoner, [generated_text])
797 + print(f"Prompt: {prompt!r}\nReasoning: {reasoning!r}\nContent: {content!r}")
798 +
799 + if "Qwen3" in model_name:
800 + assert content is not None
801 +
802 + assert reasoning is not None
803 +
804 + if content is not None:
805 + output_json = json.loads(content)
806 + jsonschema.validate(instance=output_json, schema=reasoning_schema)
807
808
809 @pytest.mark.parametrize("model_name, tokenizer_mode", PARAMS_MODELS_TOKENIZER_MODE)
810 def test_structured_output_auto_mode(
811 model_name: str,
812 tokenizer_mode: str,
813 + vllm_runner,
814 ):
815 unsupported_json_schema = UNSUPPORTED_JSON_SCHEMA
813 - llm = LLM(
814 - model=model_name,
816 + with vllm_runner(
817 + model_name,
818 max_model_len=1024,
819 structured_outputs_config=dict(backend="auto"),
820 tokenizer_mode=tokenizer_mode,
821 load_format="auto",
822 config_format="auto",
820 - )
821 -
822 - sampling_params = SamplingParams(
823 - temperature=1.0,
824 - max_tokens=1000,
825 - structured_outputs=StructuredOutputsParams(json=unsupported_json_schema),
826 - )
827 -
828 - prompts = (
829 - "Give an example JSON object for a grade "
830 - "that fits this schema: "
831 - f"{unsupported_json_schema}. Make the response as short as possible."
832 - )
833 - # This would fail with the default of "xgrammar", but in "auto"
834 - # we will handle fallback automatically.
835 - outputs = llm.generate(prompts, sampling_params=sampling_params, use_tqdm=True)
836 - # Make sure `auto` backend handling doesn't mess up sampling_params
837 - # and that we can reuse it without error.
838 - outputs.extend(
839 - llm.generate(prompts, sampling_params=sampling_params, use_tqdm=True)
840 - )
841 -
842 - assert outputs is not None
843 - for output in outputs:
844 - assert output is not None
845 - assert isinstance(output, RequestOutput)
846 - generated_text = output.outputs[0].text
847 - assert generated_text is not None
848 - print(generated_text)
823 + ) as runner:
824 + sampling_params = SamplingParams(
825 + temperature=1.0,
826 + max_tokens=1000,
827 + structured_outputs=StructuredOutputsParams(json=unsupported_json_schema),
828 + )
829 +
830 + prompts = (
831 + "Give an example JSON object for a grade "
832 + "that fits this schema: "
833 + f"{unsupported_json_schema}. Make the response as short as possible."
834 + )
835 + # This would fail with the default of "xgrammar", but in "auto"
836 + # we will handle fallback automatically.
837 + outputs = runner.llm.generate(
838 + prompts, sampling_params=sampling_params, use_tqdm=True
839 + )
840 + # Make sure `auto` backend handling doesn't mess up sampling_params
841 + # and that we can reuse it without error.
842 + outputs.extend(
843 + runner.llm.generate(prompts, sampling_params=sampling_params, use_tqdm=True)
844 + )
845 +
846 + assert outputs is not None
847 + for output in outputs:
848 + assert output is not None
849 + assert isinstance(output, RequestOutput)
850 + generated_text = output.outputs[0].text
851 + assert generated_text is not None
852 + print(generated_text)
853
850 - # Parse to verify it is valid JSON
851 - parsed_json = json.loads(generated_text)
852 - assert isinstance(parsed_json, dict)
854 + # Parse to verify it is valid JSON
855 + parsed_json = json.loads(generated_text)
856 + assert isinstance(parsed_json, dict)
857
858
855 -def test_guidance_no_additional_properties():
856 - llm = LLM(
857 - model="Qwen/Qwen2.5-1.5B-Instruct",
859 +def test_guidance_no_additional_properties(vllm_runner):
860 + with vllm_runner(
861 + "Qwen/Qwen2.5-1.5B-Instruct",
862 max_model_len=1024,
863 structured_outputs_config=dict(
864 backend="guidance",
865 disable_any_whitespace=True,
866 disable_additional_properties=True,
867 ),
864 - )
865 -
866 - schema = {
867 - "type": "object",
868 - "properties": {
869 - "a1": {"type": "string"},
870 - "a2": {"type": "string"},
871 - "a3": {"type": "string"},
872 - },
873 - "required": ["a1", "a2", "a3"],
874 - }
868 + ) as runner:
869 + schema = {
870 + "type": "object",
871 + "properties": {
872 + "a1": {"type": "string"},
873 + "a2": {"type": "string"},
874 + "a3": {"type": "string"},
875 + },
876 + "required": ["a1", "a2", "a3"],
877 + }
878
876 - prompt = (
877 - "<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a "
878 - "helpful assistant.<|im_end|>\n<|im_start|>user\nPlease generate a "
879 - "large JSON object with key-value pairs a1=b1, a2=b2, ..., a20=b20. "
880 - "Make the response as short as possible."
881 - "<|im_end|>\n<|im_start|>assistant\n"
882 - )
883 -
884 - def generate_with_backend(backend):
885 - structured_outputs_params = StructuredOutputsParams(
886 - json=schema,
887 - backend=backend,
888 - disable_any_whitespace=True,
889 - disable_additional_properties=True,
890 - )
891 - sampling_params = SamplingParams(
892 - temperature=0, max_tokens=256, structured_outputs=structured_outputs_params
879 + prompt = (
880 + "<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a "
881 + "helpful assistant.<|im_end|>\n<|im_start|>user\nPlease generate a "
882 + "large JSON object with key-value pairs a1=b1, a2=b2, ..., a20=b20. "
883 + "Make the response as short as possible."
884 + "<|im_end|>\n<|im_start|>assistant\n"
885 )
886
895 - outputs = llm.generate(prompt, sampling_params=sampling_params)
896 - assert outputs is not None
897 - generated_text = outputs[0].outputs[0].text
898 - assert generated_text is not None
899 - parsed_json = json.loads(generated_text)
900 - assert isinstance(parsed_json, dict)
901 - jsonschema.validate(instance=parsed_json, schema=schema)
902 - return parsed_json
903 -
904 - generated = generate_with_backend("guidance")
905 - assert "a1" in generated
906 - assert "a2" in generated
907 - assert "a3" in generated
908 - assert "a4" not in generated
909 - assert "a5" not in generated
910 - assert "a6" not in generated
887 + def generate_with_backend(backend):
888 + structured_outputs_params = StructuredOutputsParams(
889 + json=schema,
890 + backend=backend,
891 + disable_any_whitespace=True,
892 + disable_additional_properties=True,
893 + )
894 + sampling_params = SamplingParams(
895 + temperature=0,
896 + max_tokens=256,
897 + structured_outputs=structured_outputs_params,
898 + )
899 +
900 + outputs = runner.llm.generate(prompt, sampling_params=sampling_params)
901 + assert outputs is not None
902 + generated_text = outputs[0].outputs[0].text
903 + assert generated_text is not None
904 + parsed_json = json.loads(generated_text)
905 + assert isinstance(parsed_json, dict)
906 + jsonschema.validate(instance=parsed_json, schema=schema)
907 + return parsed_json
908 +
909 + generated = generate_with_backend("guidance")
910 + assert "a1" in generated
911 + assert "a2" in generated
912 + assert "a3" in generated
913 + assert "a4" not in generated
914 + assert "a5" not in generated
915 + assert "a6" not in generated
916
917
918 @pytest.mark.parametrize("backend", ["guidance", "xgrammar", "outlines"])
919 def test_structured_output_batched_with_non_structured_outputs_requests(
920 backend: str,
921 + vllm_runner,
922 ):
923 sample_json_schema = SAMPLE_JSON_SCHEMA
924 # Don't use eager execution on TPUs because we want to test for no
925 # recompilation at runtime
926 enforce_eager = bool(not current_platform.is_tpu())
927
922 - llm = LLM(
923 - model="meta-llama/Meta-Llama-3.1-8B-Instruct",
928 + with vllm_runner(
929 + "meta-llama/Meta-Llama-3.1-8B-Instruct",
930 enforce_eager=enforce_eager,
931 max_model_len=1024,
932 structured_outputs_config=StructuredOutputsConfig(
933 backend=backend,
934 disable_any_whitespace=backend in {"xgrammar", "guidance"},
935 ),
930 - )
931 -
932 - structured_outputs_prompt = (
933 - "Give an example JSON for an employee profile that fits this "
934 - "schema. Make the response as short as possible. Schema: "
935 - f"{sample_json_schema}"
936 - )
937 -
938 - non_structured_outputs_prompt = "The diameter of the Earth in kilometers is "
939 -
940 - prompts = [structured_outputs_prompt, non_structured_outputs_prompt]
941 - sampling_params = [
942 - SamplingParams(
943 - temperature=1.0,
944 - max_tokens=400,
945 - structured_outputs=StructuredOutputsParams(json=sample_json_schema),
946 - ),
947 - # No max tokens, temp=0 to assert on contents
948 - SamplingParams(
949 - seed=42,
950 - temperature=0,
951 - top_p=1.0,
952 - ),
953 - ]
936 + ) as runner:
937 + structured_outputs_prompt = (
938 + "Give an example JSON for an employee profile that fits this "
939 + "schema. Make the response as short as possible. Schema: "
940 + f"{sample_json_schema}"
941 + )
942
955 - outputs = llm.generate(
956 - prompts=prompts, sampling_params=sampling_params, use_tqdm=True
957 - )
943 + non_structured_outputs_prompt = "The diameter of the Earth in kilometers is "
944
959 - assert outputs is not None
945 + prompts = [structured_outputs_prompt, non_structured_outputs_prompt]
946 + sampling_params = [
947 + SamplingParams(
948 + temperature=1.0,
949 + max_tokens=400,
950 + structured_outputs=StructuredOutputsParams(json=sample_json_schema),
951 + ),
952 + # No max tokens, temp=0 to assert on contents
953 + SamplingParams(
954 + seed=42,
955 + temperature=0,
956 + top_p=1.0,
957 + ),
958 + ]
959
961 - # Free memory as soon as possible as failed assertions
962 - # will short circuit and not free up memory
963 - del llm
964 - torch.accelerator.empty_cache()
965 - cleanup_dist_env_and_memory()
960 + outputs = runner.llm.generate(
961 + prompts=prompts, sampling_params=sampling_params, use_tqdm=True
962 + )
963
967 - for index, output in enumerate(outputs):
968 - assert output is not None
969 - assert isinstance(output, RequestOutput)
970 - prompt = output.prompt
964 + assert outputs is not None
965
972 - generated_text = output.outputs[0].text
973 - assert generated_text is not None
974 - print(f"Prompt:\n{prompt!r}\nGenerated text:\n{generated_text!r}")
966 + for index, output in enumerate(outputs):
967 + assert output is not None
968 + assert isinstance(output, RequestOutput)
969 + prompt = output.prompt
970
976 - if index == 0:
977 - # First prompt is structured outputs, expect valid JSON
978 - assert "\n" not in generated_text
979 - output_json = json.loads(generated_text)
980 - jsonschema.validate(instance=output_json, schema=sample_json_schema)
981 - else:
982 - # Second prompt is not structured outputs, expect valid output
983 - # Cannot assert on exact output, but we can expect it to be factual
984 - assert "12,742" in generated_text
971 + generated_text = output.outputs[0].text
972 + assert generated_text is not None
973 + print(f"Prompt:\n{prompt!r}\nGenerated text:\n{generated_text!r}")
974
986 - # non-structured outputs requests should not return a valid JSON here
987 - with pytest.raises(ValueError):
975 + if index == 0:
976 + # First prompt is structured outputs, expect valid JSON
977 + assert "\n" not in generated_text
978 output_json = json.loads(generated_text)
979 + jsonschema.validate(instance=output_json, schema=sample_json_schema)
980 + else:
981 + # Second prompt is not structured outputs, expect valid output
982 + # Cannot assert on exact output, but we can expect it to be factual
983 + assert "12,742" in generated_text
984 +
985 + # non-structured outputs requests should not return a valid JSON here
986 + with pytest.raises(ValueError):
987 + output_json = json.loads(generated_text)
988
989
990 @pytest.mark.parametrize("backend", ["xgrammar"])
992 -def test_structured_output_with_structural_tag(backend: str):
993 - llm = LLM(
994 - model="Qwen/Qwen2.5-1.5B-Instruct",
991 +def test_structured_output_with_structural_tag(backend: str, vllm_runner):
992 + with vllm_runner(
993 + "Qwen/Qwen2.5-1.5B-Instruct",
994 structured_outputs_config=StructuredOutputsConfig(backend=backend),
996 - )
997 -
998 - structural_tag_config = {
999 - "type": "structural_tag",
1000 - "format": {
1001 - "type": "triggered_tags",
1002 - "tags": [
1003 - {"begin": "hello_flag", "content": {"type": "any_text"}, "end": "hello"}
1004 - ],
1005 - "triggers": ["hello"],
1006 - "stop_after_first": False,
1007 - },
1008 - }
995 + ) as runner:
996 + structural_tag_config = {
997 + "type": "structural_tag",
998 + "format": {
999 + "type": "triggered_tags",
1000 + "tags": [
1001 + {
1002 + "begin": "hello_flag",
1003 + "content": {"type": "any_text"},
1004 + "end": "hello",
1005 + }
1006 + ],
1007 + "triggers": ["hello"],
1008 + "stop_after_first": False,
1009 + },
1010 + }
1011
1010 - sampling_params = SamplingParams(
1011 - temperature=0.0,
1012 - max_tokens=500,
1013 - structured_outputs=StructuredOutputsParams(
1014 - structural_tag=json.dumps(structural_tag_config)
1015 - ),
1016 - )
1017 -
1018 - prompt = "Hello and repeat hello 10 times, do not say anything else. Only say hello hello hello, now start"
1019 - outputs = llm.generate(prompt, sampling_params=sampling_params, use_tqdm=True)
1020 - assert outputs is not None
1021 - for output in outputs:
1022 - assert output is not None
1023 - assert isinstance(output, RequestOutput)
1024 - prompt = output.prompt
1025 - generated_text = output.outputs[0].text
1026 - assert generated_text is not None
1027 - assert "hello_flag" in generated_text, (
1028 - f"Expected 'hello_flag' to be in generated text, but got: {generated_text}"
1012 + sampling_params = SamplingParams(
1013 + temperature=0.0,
1014 + max_tokens=500,
1015 + structured_outputs=StructuredOutputsParams(
1016 + structural_tag=json.dumps(structural_tag_config)
1017 + ),
1018 )
1019 +
1020 + prompt = "Hello and repeat hello 10 times, do not say anything else. Only say hello hello hello, now start"
1021 + outputs = runner.llm.generate(
1022 + prompt, sampling_params=sampling_params, use_tqdm=True
1023 + )
1024 + assert outputs is not None
1025 + for output in outputs:
1026 + assert output is not None
1027 + assert isinstance(output, RequestOutput)
1028 + prompt = output.prompt
1029 + generated_text = output.outputs[0].text
1030 + assert generated_text is not None
1031 + assert "hello_flag" in generated_text, (
1032 + f"Expected 'hello_flag' to be in generated text, but got: {generated_text}"
1033 + )
tests/entrypoints/multimodal/conftest.py
-73
@@ -1,11 +1,5 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from collections.abc import Callable, Iterator
4 -from contextlib import contextmanager
5 -from typing import Any
6 -
7 -import pytest
8 -
3 # Test different image extensions (JPG/PNG) and formats (gray/RGB/RGBA)
4 TEST_IMAGE_ASSETS = [
5 "2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", # "https://vllm-public-assets.s3.us-west-2.amazonaws.com/vision_model_images/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
@@ -13,70 +7,3 @@ TEST_IMAGE_ASSETS = [
7 "1280px-Venn_diagram_rgb.svg.png", # "https://vllm-public-assets.s3.us-west-2.amazonaws.com/vision_model_images/1280px-Venn_diagram_rgb.svg.png",
8 "RGBA_comp.png", # "https://vllm-public-assets.s3.us-west-2.amazonaws.com/vision_model_images/RGBA_comp.png",
9 ]
16 -
17 -
18 -def _shutdown_llm(llm: Any, gpu_memory_utilization: float) -> None:
19 - from vllm.distributed import cleanup_dist_env_and_memory
20 - from vllm.platforms import current_platform
21 -
22 - try:
23 - shutdown_timeout = 60.0 if current_platform.is_rocm() else None
24 - llm.llm_engine.engine_core.shutdown(timeout=shutdown_timeout)
25 - except Exception:
26 - pass
27 -
28 - del llm
29 -
30 - try:
31 - import torch
32 -
33 - torch._dynamo.reset()
34 - except Exception:
35 - pass
36 -
37 - cleanup_dist_env_and_memory()
38 -
39 - if current_platform.is_rocm():
40 - from tests.utils import wait_for_rocm_memory_to_settle
41 -
42 - wait_for_rocm_memory_to_settle(threshold_ratio=1.0 - gpu_memory_utilization)
43 -
44 -
45 -@contextmanager
46 -def managed_llm(*args: Any, **kwargs: Any) -> Iterator[Any]:
47 - from vllm import LLM
48 -
49 - llm = LLM(*args, **kwargs)
50 - gpu_memory_utilization = (
51 - llm.llm_engine.vllm_config.cache_config.gpu_memory_utilization
52 - )
53 - try:
54 - yield llm
55 - finally:
56 - _shutdown_llm(llm, gpu_memory_utilization)
57 -
58 -
59 -def _make_managed_llm_factory() -> Iterator[Callable[..., Any]]:
60 - from vllm import LLM
61 -
62 - llms: list[tuple[Any, float]] = []
63 -
64 - def make_llm(*args: Any, **kwargs: Any) -> Any:
65 - llm = LLM(*args, **kwargs)
66 - gpu_memory_utilization = (
67 - llm.llm_engine.vllm_config.cache_config.gpu_memory_utilization
68 - )
69 - llms.append((llm, gpu_memory_utilization))
70 - return llm
71 -
72 - try:
73 - yield make_llm
74 - finally:
75 - while llms:
76 - llm, gpu_memory_utilization = llms.pop()
77 - _shutdown_llm(llm, gpu_memory_utilization)
78 -
79 -
80 -@pytest.fixture
81 -def multimodal_llm_factory() -> Iterator[Callable[..., Any]]:
82 - yield from _make_managed_llm_factory()
tests/entrypoints/multimodal/llm/test_chat.py
+9 -4
@@ -1,21 +1,26 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import weakref
4 +
5 import pytest
6
7 from tests.entrypoints.multimodal.conftest import TEST_IMAGE_ASSETS
8
9
10 @pytest.fixture(scope="function")
9 -def vision_llm(multimodal_llm_factory):
10 - return multimodal_llm_factory(
11 - model="microsoft/Phi-3.5-vision-instruct",
11 +def vision_llm(vllm_runner):
12 + with vllm_runner(
13 + "microsoft/Phi-3.5-vision-instruct",
14 max_model_len=4096,
15 max_num_seqs=5,
16 enforce_eager=True,
17 trust_remote_code=True,
18 limit_mm_per_prompt={"image": 2},
19 seed=0,
18 - )
20 + ) as runner:
21 + # pytest caches yielded fixtures until after teardown, so use a proxy to
22 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
23 + yield weakref.proxy(runner.llm)
24
25
26 @pytest.mark.parametrize(
tests/entrypoints/multimodal/llm/test_mm_cache_external_injection.py
+91 -93
@@ -69,7 +69,7 @@ def test_inject_into_mm_cache(
69 image_urls,
70 mm_processor_cache_type,
71 caplog_vllm,
72 - multimodal_llm_factory,
72 + vllm_runner,
73 ):
74 """Test that inject_into_mm_cache() injects pre-processed mm_kwargs into
75 the processor cache and MM cache hit metrics are updated correctly.
@@ -79,117 +79,115 @@ def test_inject_into_mm_cache(
79 2. Extract cached kwargs, call inject_into_mm_cache with a new hash,
80 then generate with a pre-rendered input -> verifies injection works
81 """
82 - llm = multimodal_llm_factory(
83 - model="llava-hf/llava-1.5-7b-hf",
82 + with vllm_runner(
83 + "llava-hf/llava-1.5-7b-hf",
84 max_model_len=4096,
85 max_num_seqs=5,
86 enforce_eager=True,
87 disable_log_stats=False,
88 limit_mm_per_prompt={"image": 2},
89 mm_processor_cache_type=mm_processor_cache_type,
90 - )
91 -
92 - # Step 1: Normal requests to populate the cache
93 - llm.chat(_make_messages(image_urls[0]))
94 - assert _get_mm_cache_stats(llm.get_metrics()) == (1, 0)
95 -
96 - llm.chat(_make_messages(image_urls[0]))
97 - assert _get_mm_cache_stats(llm.get_metrics()) == (2, 1)
98 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(50.0)
99 -
100 - # Step 2: Use a second image to get valid expanded tokens and
101 - # placeholder positions via the renderer.
102 - llm.chat(_make_messages(image_urls[1]))
103 - queries_before = _get_mm_cache_stats(llm.get_metrics())[0] # 3
104 -
105 - renderer = llm.llm_engine.renderer
106 - cache = renderer.mm_processor_cache
107 - assert cache is not None, "Processor cache should be enabled"
108 -
109 - _, eng_prompts = renderer.render_chat(
110 - [_make_messages(image_urls[1])],
111 - ChatParams(),
112 - )
113 - eng_input = eng_prompts[0]
114 -
115 - # Inject pre-processed mm_kwargs with a NEW hash via public API
116 - new_mm_hash = "deadbeef" * 8
117 - mm_hashes = {"image": [new_mm_hash]}
118 - mm_kwargs = eng_input["mm_kwargs"]
119 -
120 - llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
121 -
122 - # Build pre-rendered input (no externally_processed flag needed)
123 - pre_rendered_input = {
124 - "type": "multimodal",
125 - "prompt_token_ids": eng_input["prompt_token_ids"],
126 - "mm_kwargs": mm_kwargs,
127 - "mm_hashes": mm_hashes,
128 - "mm_placeholders": eng_input["mm_placeholders"],
129 - }
130 -
131 - llm.generate(
132 - pre_rendered_input,
133 - sampling_params=SamplingParams(max_tokens=1),
134 - )
135 -
136 - # Verify cache was queried and injection happened
137 - queries_after = _get_mm_cache_stats(llm.get_metrics())[0]
138 - assert queries_after > queries_before, (
139 - "Cache should have been queried for the injected item"
140 - )
141 - mm_rate = _get_mm_cache_log(llm, caplog_vllm)
142 - assert mm_rate >= 0.0, "MM cache hit rate should be reported"
90 + ) as runner:
91 + # Step 1: Normal requests to populate the cache
92 + runner.llm.chat(_make_messages(image_urls[0]))
93 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (1, 0)
94 +
95 + runner.llm.chat(_make_messages(image_urls[0]))
96 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (2, 1)
97 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(50.0)
98 +
99 + # Step 2: Use a second image to get valid expanded tokens and
100 + # placeholder positions via the renderer.
101 + runner.llm.chat(_make_messages(image_urls[1]))
102 + queries_before = _get_mm_cache_stats(runner.llm.get_metrics())[0] # 3
103 +
104 + renderer = runner.llm.llm_engine.renderer
105 + cache = renderer.mm_processor_cache
106 + assert cache is not None, "Processor cache should be enabled"
107 +
108 + _, eng_prompts = renderer.render_chat(
109 + [_make_messages(image_urls[1])],
110 + ChatParams(),
111 + )
112 + eng_input = eng_prompts[0]
113 +
114 + # Inject pre-processed mm_kwargs with a NEW hash via public API
115 + new_mm_hash = "deadbeef" * 8
116 + mm_hashes = {"image": [new_mm_hash]}
117 + mm_kwargs = eng_input["mm_kwargs"]
118 +
119 + runner.llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
120 +
121 + # Build pre-rendered input (no externally_processed flag needed)
122 + pre_rendered_input = {
123 + "type": "multimodal",
124 + "prompt_token_ids": eng_input["prompt_token_ids"],
125 + "mm_kwargs": mm_kwargs,
126 + "mm_hashes": mm_hashes,
127 + "mm_placeholders": eng_input["mm_placeholders"],
128 + }
129 +
130 + runner.llm.generate(
131 + pre_rendered_input,
132 + sampling_params=SamplingParams(max_tokens=1),
133 + )
134 +
135 + # Verify cache was queried and injection happened
136 + queries_after = _get_mm_cache_stats(runner.llm.get_metrics())[0]
137 + assert queries_after > queries_before, (
138 + "Cache should have been queried for the injected item"
139 + )
140 + mm_rate = _get_mm_cache_log(runner.llm, caplog_vllm)
141 + assert mm_rate >= 0.0, "MM cache hit rate should be reported"
142
143
144 @pytest.mark.parametrize("image_urls", [TEST_IMAGE_ASSETS[:1]], indirect=True)
145 def test_inject_into_mm_cache_without_cache(
146 num_gpus_available,
147 image_urls,
149 - multimodal_llm_factory,
148 + vllm_runner,
149 ):
150 """Test that inject_into_mm_cache works gracefully when processor cache
151 is disabled (mm_processor_cache_gb=0). Should not crash.
152 """
154 - llm = multimodal_llm_factory(
155 - model="llava-hf/llava-1.5-7b-hf",
153 + with vllm_runner(
154 + "llava-hf/llava-1.5-7b-hf",
155 max_model_len=4096,
156 max_num_seqs=5,
157 enforce_eager=True,
158 disable_log_stats=False,
159 limit_mm_per_prompt={"image": 2},
160 mm_processor_cache_gb=0,
162 - )
163 -
164 - # Run a normal chat request first to warm up the model.
165 - llm.chat(_make_messages(image_urls[0]))
166 -
167 - # Use the renderer to get a proper EngineInput with expanded tokens
168 - renderer = llm.llm_engine.renderer
169 - _, eng_prompts = renderer.render_chat(
170 - [_make_messages(image_urls[0])],
171 - ChatParams(),
172 - )
173 - eng_input = eng_prompts[0]
174 -
175 - mm_hashes = {"image": ["abcd1234" * 8]}
176 - mm_kwargs = eng_input["mm_kwargs"]
177 -
178 - # inject_into_mm_cache should not crash even without cache
179 - llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
180 -
181 - # Build and generate with pre-rendered input
182 - pre_rendered_input = {
183 - "type": "multimodal",
184 - "prompt_token_ids": eng_input["prompt_token_ids"],
185 - "mm_kwargs": mm_kwargs,
186 - "mm_hashes": mm_hashes,
187 - "mm_placeholders": eng_input["mm_placeholders"],
188 - }
189 -
190 - result = llm.generate(
191 - pre_rendered_input,
192 - sampling_params=SamplingParams(max_tokens=1),
193 - )
194 - assert len(result) == 1, "Should produce one output"
195 - assert len(result[0].outputs) >= 1, "Should have at least one output sequence"
161 + ) as runner:
162 + # Run a normal chat request first to warm up the model.
163 + runner.llm.chat(_make_messages(image_urls[0]))
164 +
165 + # Use the renderer to get a proper EngineInput with expanded tokens
166 + renderer = runner.llm.llm_engine.renderer
167 + _, eng_prompts = renderer.render_chat(
168 + [_make_messages(image_urls[0])],
169 + ChatParams(),
170 + )
171 + eng_input = eng_prompts[0]
172 +
173 + mm_hashes = {"image": ["abcd1234" * 8]}
174 + mm_kwargs = eng_input["mm_kwargs"]
175 +
176 + # inject_into_mm_cache should not crash even without cache
177 + runner.llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
178 +
179 + # Build and generate with pre-rendered input
180 + pre_rendered_input = {
181 + "type": "multimodal",
182 + "prompt_token_ids": eng_input["prompt_token_ids"],
183 + "mm_kwargs": mm_kwargs,
184 + "mm_hashes": mm_hashes,
185 + "mm_placeholders": eng_input["mm_placeholders"],
186 + }
187 +
188 + result = runner.llm.generate(
189 + pre_rendered_input,
190 + sampling_params=SamplingParams(max_tokens=1),
191 + )
192 + assert len(result) == 1, "Should produce one output"
193 + assert len(result[0].outputs) >= 1, "Should have at least one output sequence"
tests/entrypoints/multimodal/llm/test_mm_cache_stats.py
+27 -28
@@ -61,38 +61,37 @@ def test_mm_cache_stats(
61 image_urls,
62 mm_processor_cache_type,
63 caplog_vllm,
64 - multimodal_llm_factory,
64 + vllm_runner,
65 ):
66 - llm = multimodal_llm_factory(
67 - model="llava-hf/llava-1.5-7b-hf",
66 + with vllm_runner(
67 + "llava-hf/llava-1.5-7b-hf",
68 max_model_len=4096,
69 max_num_seqs=5,
70 enforce_eager=True,
71 mm_processor_cache_type=mm_processor_cache_type,
72 disable_log_stats=False,
73 limit_mm_per_prompt={"image": 2},
74 - )
75 -
76 - llm.chat(_make_messages(image_urls[0]))
77 - assert _get_mm_cache_stats(llm.get_metrics()) == (1, 0)
78 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(0.0)
79 -
80 - llm.chat(_make_messages(image_urls[1]))
81 - assert _get_mm_cache_stats(llm.get_metrics()) == (2, 0)
82 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(0.0)
83 -
84 - llm.chat(_make_messages(image_urls[0]))
85 - assert _get_mm_cache_stats(llm.get_metrics()) == (3, 1)
86 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(33.3)
87 -
88 - # NOTE: This only resets hit rate stats in CachingMetrics
89 - # The raw queries and hits counts remain unaffected
90 - llm.reset_mm_cache()
91 -
92 - llm.chat(_make_messages(image_urls[0]))
93 - assert _get_mm_cache_stats(llm.get_metrics()) == (4, 1)
94 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(0.0)
95 -
96 - llm.chat(_make_messages(image_urls[1]))
97 - assert _get_mm_cache_stats(llm.get_metrics()) == (5, 1)
98 - assert _get_mm_cache_log(llm, caplog_vllm) == pytest.approx(0.0)
74 + ) as runner:
75 + runner.llm.chat(_make_messages(image_urls[0]))
76 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (1, 0)
77 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(0.0)
78 +
79 + runner.llm.chat(_make_messages(image_urls[1]))
80 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (2, 0)
81 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(0.0)
82 +
83 + runner.llm.chat(_make_messages(image_urls[0]))
84 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (3, 1)
85 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(33.3)
86 +
87 + # NOTE: This only resets hit rate stats in CachingMetrics
88 + # The raw queries and hits counts remain unaffected
89 + runner.llm.reset_mm_cache()
90 +
91 + runner.llm.chat(_make_messages(image_urls[0]))
92 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (4, 1)
93 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(0.0)
94 +
95 + runner.llm.chat(_make_messages(image_urls[1]))
96 + assert _get_mm_cache_stats(runner.llm.get_metrics()) == (5, 1)
97 + assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(0.0)
tests/entrypoints/multimodal/llm/test_mm_embeds_only.py
+9 -6
@@ -1,9 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +import weakref
5 +
6 import pytest
7
6 -from tests.entrypoints.multimodal.conftest import managed_llm
8 from vllm import LLM, SamplingParams
9 from vllm.assets.image import ImageAsset
10 from vllm.exceptions import VLLMValidationError
@@ -14,17 +15,19 @@ TEXT_ONLY_PROMPT = "USER: What is 2 + 2?\nASSISTANT:"
15
16
17 @pytest.fixture(scope="module")
17 -def llm():
18 +def llm(vllm_runner):
19 """LLM with enable_mm_embeds=True and all modality limits zeroed out."""
19 - with managed_llm(
20 - model=MODEL,
20 + with vllm_runner(
21 + MODEL,
22 max_model_len=2048,
23 enforce_eager=True,
24 gpu_memory_utilization=0.8,
25 enable_mm_embeds=True,
26 limit_mm_per_prompt={"image": 0},
26 - ) as llm:
27 - yield llm
27 + ) as runner:
28 + # pytest caches yielded fixtures until after teardown, so use a proxy to
29 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
30 + yield weakref.proxy(runner.llm)
31
32
33 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/basic/test_encode.py
+9 -13
@@ -6,7 +6,6 @@ import weakref
6 import pytest
7
8 from vllm import LLM, PoolingParams
9 -from vllm.distributed import cleanup_dist_env_and_memory
9 from vllm.exceptions import VLLMValidationError
10
11 MODEL_NAME = "intfloat/multilingual-e5-small"
@@ -29,23 +28,20 @@ TOKEN_IDS = [
28
29
30 @pytest.fixture(scope="module")
32 -def llm():
33 - # pytest caches the fixture so we use weakref.proxy to
34 - # enable garbage collection
35 - llm = LLM(
36 - model=MODEL_NAME,
31 +def llm(vllm_runner):
32 + with vllm_runner(
33 + MODEL_NAME,
34 + max_model_len=None,
35 max_num_batched_tokens=32768,
36 tensor_parallel_size=1,
37 gpu_memory_utilization=0.75,
38 enforce_eager=True,
39 seed=0,
42 - )
43 -
44 - yield weakref.proxy(llm)
45 -
46 - del llm
47 -
48 - cleanup_dist_env_and_memory()
40 + enable_chunked_prefill=None,
41 + ) as runner:
42 + # pytest caches yielded fixtures until after teardown, so use a proxy to
43 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
44 + yield weakref.proxy(runner.llm)
45
46
47 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/basic/test_tiling_engine.py
+10 -12
@@ -6,28 +6,26 @@ from unittest import mock
6
7 import pytest
8
9 -from vllm import LLM, PoolingParams
10 -from vllm.distributed import cleanup_dist_env_and_memory
9 +from vllm import PoolingParams
10
11 MODEL_NAME = "intfloat/multilingual-e5-small"
12
13
14 @pytest.fixture(scope="module")
16 -def llm():
17 - llm = LLM(
18 - model=MODEL_NAME,
15 +def llm(vllm_runner):
16 + with vllm_runner(
17 + MODEL_NAME,
18 + max_model_len=None,
19 max_num_seqs=2, # small to trigger tiling
20 tensor_parallel_size=1,
21 gpu_memory_utilization=0.75,
22 enforce_eager=True,
23 seed=0,
24 - )
25 -
26 - yield weakref.proxy(llm)
27 -
28 - del llm
29 -
30 - cleanup_dist_env_and_memory()
24 + enable_chunked_prefill=None,
25 + ) as runner:
26 + # pytest caches yielded fixtures until after teardown, so use a proxy to
27 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
28 + yield weakref.proxy(runner.llm)
29
30
31 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/classify/test_offline.py
+9 -13
@@ -7,7 +7,6 @@ import torch
7
8 from tests.models.utils import softmax
9 from vllm import LLM, ClassificationRequestOutput, PoolingParams
10 -from vllm.distributed import cleanup_dist_env_and_memory
10 from vllm.tasks import PoolingTask
11
12 MODEL_NAME = "jason9693/Qwen2.5-1.5B-apeach"
@@ -18,23 +17,20 @@ num_labels = 2
17
18
19 @pytest.fixture(scope="module")
21 -def llm():
22 - # pytest caches the fixture so we use weakref.proxy to
23 - # enable garbage collection
24 - llm = LLM(
25 - model=MODEL_NAME,
20 +def llm(vllm_runner):
21 + with vllm_runner(
22 + MODEL_NAME,
23 + max_model_len=None,
24 max_num_batched_tokens=32768,
25 tensor_parallel_size=1,
26 gpu_memory_utilization=0.75,
27 enforce_eager=True,
28 seed=0,
31 - )
32 -
33 - yield weakref.proxy(llm)
34 -
35 - del llm
36 -
37 - cleanup_dist_env_and_memory()
29 + enable_chunked_prefill=None,
30 + ) as runner:
31 + # pytest caches yielded fixtures until after teardown, so use a proxy to
32 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
33 + yield weakref.proxy(runner.llm)
34
35
36 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/embed/test_offline.py
+10 -13
@@ -7,7 +7,6 @@ import torch
7 import torch.nn.functional as F
8
9 from vllm import LLM, EmbeddingRequestOutput, PoolingParams
10 -from vllm.distributed import cleanup_dist_env_and_memory
10 from vllm.tasks import PoolingTask
11
12 MODEL_NAME = "intfloat/multilingual-e5-small"
@@ -18,23 +17,21 @@ embedding_size = 384
17
18
19 @pytest.fixture(scope="module")
21 -def llm():
22 - # pytest caches the fixture so we use weakref.proxy to
23 - # enable garbage collection
24 - llm = LLM(
25 - model=MODEL_NAME,
20 +def llm(vllm_runner):
21 + with vllm_runner(
22 + MODEL_NAME,
23 + max_model_len=None,
24 max_num_batched_tokens=32768,
25 tensor_parallel_size=1,
26 gpu_memory_utilization=0.75,
27 enforce_eager=True,
28 seed=0,
31 - )
32 - assert embedding_size == llm.model_config.embedding_size
33 -
34 - yield weakref.proxy(llm)
35 -
36 - del llm
37 - cleanup_dist_env_and_memory()
29 + enable_chunked_prefill=None,
30 + ) as runner:
31 + assert embedding_size == runner.llm.model_config.embedding_size
32 + # pytest caches yielded fixtures until after teardown, so use a proxy to
33 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
34 + yield weakref.proxy(runner.llm)
35
36
37 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/reward/test_token_reward_offline.py
+9 -13
@@ -8,7 +8,6 @@ import torch
8
9 from tests.models.utils import softmax
10 from vllm import LLM, PoolingParams
11 -from vllm.distributed import cleanup_dist_env_and_memory
11
12 MODEL_NAME = "internlm/internlm2-1_8b-reward"
13
@@ -16,24 +15,21 @@ prompts = ["The chef prepared a delicious meal."]
15
16
17 @pytest.fixture(scope="module")
19 -def llm():
20 - # pytest caches the fixture so we use weakref.proxy to
21 - # enable garbage collection
22 - llm = LLM(
23 - model=MODEL_NAME,
18 +def llm(vllm_runner):
19 + with vllm_runner(
20 + MODEL_NAME,
21 + max_model_len=None,
22 max_num_batched_tokens=32768,
23 tensor_parallel_size=1,
24 gpu_memory_utilization=0.75,
25 enforce_eager=True,
26 trust_remote_code=True,
27 seed=0,
30 - )
31 -
32 - yield weakref.proxy(llm)
33 -
34 - del llm
35 -
36 - cleanup_dist_env_and_memory()
28 + enable_chunked_prefill=None,
29 + ) as runner:
30 + # pytest caches yielded fixtures until after teardown, so use a proxy to
31 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
32 + yield weakref.proxy(runner.llm)
33
34
35 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/scoring/test_bi_encoder_offline.py
+9 -14
@@ -6,8 +6,6 @@ import weakref
6 import pytest
7
8 from tests.entrypoints.pooling.scoring.util import EncoderScoringHfRunner
9 -from vllm import LLM
10 -from vllm.distributed import cleanup_dist_env_and_memory
9
10 MODEL_NAME = "intfloat/multilingual-e5-small"
11 PROMPT = "The chef prepared a delicious meal."
@@ -27,23 +25,20 @@ DTYPE = "half"
25
26
27 @pytest.fixture(scope="module")
30 -def llm():
31 - # pytest caches the fixture so we use weakref.proxy to
32 - # enable garbage collection
33 - llm = LLM(
34 - model=MODEL_NAME,
28 +def llm(vllm_runner):
29 + with vllm_runner(
30 + MODEL_NAME,
31 + max_model_len=None,
32 max_num_batched_tokens=32768,
33 tensor_parallel_size=1,
34 gpu_memory_utilization=0.75,
35 enforce_eager=True,
36 seed=0,
40 - )
41 -
42 - yield weakref.proxy(llm)
43 -
44 - del llm
45 -
46 - cleanup_dist_env_and_memory()
37 + enable_chunked_prefill=None,
38 + ) as runner:
39 + # pytest caches yielded fixtures until after teardown, so use a proxy to
40 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
41 + yield weakref.proxy(runner.llm)
42
43
44 @pytest.fixture(scope="module")
tests/entrypoints/pooling/scoring/test_cross_encoder_offline.py
+9 -13
@@ -8,7 +8,6 @@ import torch
8
9 from tests.models.utils import softmax
10 from vllm import LLM, PoolingParams
11 -from vllm.distributed import cleanup_dist_env_and_memory
11
12 MODEL_NAME = "tomaarsen/Qwen3-Reranker-0.6B-seq-cls"
13 PROMPT = "The chef prepared a delicious meal."
@@ -24,23 +23,20 @@ TEXTS_2 = [
23
24
25 @pytest.fixture(scope="module")
27 -def llm():
28 - # pytest caches the fixture so we use weakref.proxy to
29 - # enable garbage collection
30 - llm = LLM(
31 - model=MODEL_NAME,
26 +def llm(vllm_runner):
27 + with vllm_runner(
28 + MODEL_NAME,
29 + max_model_len=None,
30 max_num_batched_tokens=32768,
31 tensor_parallel_size=1,
32 gpu_memory_utilization=0.75,
33 enforce_eager=True,
34 seed=0,
37 - )
38 -
39 - yield weakref.proxy(llm)
40 -
41 - del llm
42 -
43 - cleanup_dist_env_and_memory()
35 + enable_chunked_prefill=None,
36 + ) as runner:
37 + # pytest caches yielded fixtures until after teardown, so use a proxy to
38 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
39 + yield weakref.proxy(runner.llm)
40
41
42 @pytest.fixture(scope="module")
tests/entrypoints/pooling/scoring/test_late_interaction_offline.py
+9 -15
@@ -5,9 +5,6 @@ import weakref
5
6 import pytest
7
8 -from vllm import LLM
9 -from vllm.distributed import cleanup_dist_env_and_memory
10 -
8 from .util import ColBERTScoringHfRunner
9
10 MODEL_NAME = "answerdotai/answerai-colbert-small-v1"
@@ -30,23 +27,20 @@ DTYPE = "half"
27
28
29 @pytest.fixture(scope="module")
33 -def llm():
34 - # pytest caches the fixture so we use weakref.proxy to
35 - # enable garbage collection
36 - llm = LLM(
37 - model=MODEL_NAME,
30 +def llm(vllm_runner):
31 + with vllm_runner(
32 + MODEL_NAME,
33 + max_model_len=None,
34 max_num_batched_tokens=32768,
35 tensor_parallel_size=1,
36 gpu_memory_utilization=0.75,
37 enforce_eager=True,
38 seed=0,
43 - )
44 -
45 - yield weakref.proxy(llm)
46 -
47 - del llm
48 -
49 - cleanup_dist_env_and_memory()
39 + enable_chunked_prefill=None,
40 + ) as runner:
41 + # pytest caches yielded fixtures until after teardown, so use a proxy to
42 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
43 + yield weakref.proxy(runner.llm)
44
45
46 @pytest.fixture(scope="module")
tests/entrypoints/pooling/scoring/test_late_interaction_offline_vision.py
+9 -15
@@ -5,32 +5,26 @@ import weakref
5
6 import pytest
7
8 -from vllm import LLM
9 -from vllm.distributed import cleanup_dist_env_and_memory
10 -
8 from .util import make_base64_image, make_image_mm_param
9
10 MODEL_NAME = "vidore/colpali-v1.3-hf"
11
12
13 @pytest.fixture(scope="module")
17 -def llm():
18 - # pytest caches the fixture so we use weakref.proxy to
19 - # enable garbage collection
20 - llm = LLM(
21 - model=MODEL_NAME,
14 +def llm(vllm_runner):
15 + with vllm_runner(
16 + MODEL_NAME,
17 + max_model_len=None,
18 max_num_batched_tokens=32768,
19 tensor_parallel_size=1,
20 gpu_memory_utilization=0.75,
21 enforce_eager=True,
22 seed=0,
27 - )
28 -
29 - yield weakref.proxy(llm)
30 -
31 - del llm
32 -
33 - cleanup_dist_env_and_memory()
23 + enable_chunked_prefill=None,
24 + ) as runner:
25 + # pytest caches yielded fixtures until after teardown, so use a proxy to
26 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
27 + yield weakref.proxy(runner.llm)
28
29
30 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/token_classify/test_offline.py
+9 -13
@@ -6,7 +6,6 @@ import pytest
6
7 from vllm import LLM, PoolingRequestOutput
8 from vllm.config import PoolerConfig
9 -from vllm.distributed import cleanup_dist_env_and_memory
9 from vllm.tasks import PoolingTask
10
11 MODEL_NAME = "jason9693/Qwen2.5-1.5B-apeach"
@@ -17,24 +16,21 @@ num_labels = 2
16
17
18 @pytest.fixture(scope="module")
20 -def llm():
21 - # pytest caches the fixture so we use weakref.proxy to
22 - # enable garbage collection
23 - llm = LLM(
24 - model=MODEL_NAME,
19 +def llm(vllm_runner):
20 + with vllm_runner(
21 + MODEL_NAME,
22 + max_model_len=None,
23 pooler_config=PoolerConfig(task="token_classify"),
24 max_num_batched_tokens=32768,
25 tensor_parallel_size=1,
26 gpu_memory_utilization=0.75,
27 enforce_eager=True,
28 seed=0,
31 - )
32 -
33 - yield weakref.proxy(llm)
34 -
35 - del llm
36 -
37 - cleanup_dist_env_and_memory()
29 + enable_chunked_prefill=None,
30 + ) as runner:
31 + # pytest caches yielded fixtures until after teardown, so use a proxy to
32 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
33 + yield weakref.proxy(runner.llm)
34
35
36 @pytest.mark.skip_global_cleanup
tests/entrypoints/pooling/token_embed/test_offline.py
+10 -13
@@ -6,7 +6,6 @@ import pytest
6
7 from vllm import LLM, PoolingRequestOutput
8 from vllm.config import PoolerConfig
9 -from vllm.distributed import cleanup_dist_env_and_memory
9 from vllm.tasks import PoolingTask
10
11 MODEL_NAME = "intfloat/multilingual-e5-small"
@@ -17,24 +16,22 @@ embedding_size = 384
16
17
18 @pytest.fixture(scope="module")
20 -def llm():
21 - # pytest caches the fixture so we use weakref.proxy to
22 - # enable garbage collection
23 - llm = LLM(
24 - model=MODEL_NAME,
19 +def llm(vllm_runner):
20 + with vllm_runner(
21 + MODEL_NAME,
22 + max_model_len=None,
23 pooler_config=PoolerConfig(task="token_embed"),
24 max_num_batched_tokens=32768,
25 tensor_parallel_size=1,
26 gpu_memory_utilization=0.75,
27 enforce_eager=True,
28 seed=0,
31 - )
32 - assert embedding_size == llm.model_config.embedding_size
33 -
34 - yield weakref.proxy(llm)
35 -
36 - del llm
37 - cleanup_dist_env_and_memory()
29 + enable_chunked_prefill=None,
30 + ) as runner:
31 + assert embedding_size == runner.llm.model_config.embedding_size
32 + # pytest caches yielded fixtures until after teardown, so use a proxy to
33 + # avoid retaining the LLM while VllmRunner.__exit__ releases ROCm memory.
34 + yield weakref.proxy(runner.llm)
35
36
37 @pytest.mark.skip_global_cleanup
tests/entrypoints/weight_transfer/test_weight_transfer_llm.py
+47 -40
@@ -8,13 +8,13 @@ actual NCCL communication.
8 """
9
10 import os
11 +import weakref
12 from dataclasses import dataclass
13 from unittest.mock import patch
14
15 import pytest
16 import torch
17
17 -from vllm import LLM
18 from vllm.config import WeightTransferConfig
19 from vllm.distributed.weight_transfer.base import (
20 WeightTransferEngine,
@@ -102,25 +102,24 @@ def mock_create_engine(config, vllm_config, device, model):
102
103
104 @create_new_process_for_each_test()
105 -def test_get_world_size_tp1():
105 +def test_get_world_size_tp1(vllm_runner):
106 """Test world_size is correctly configured for TP=1."""
107 if torch.accelerator.device_count() < 1:
108 pytest.skip("Need at least 1 GPU for this test")
109
110 - llm = LLM(
111 - model=MODEL_NAME,
110 + with vllm_runner(
111 + MODEL_NAME,
112 enforce_eager=True,
113 load_format="dummy",
114 tensor_parallel_size=1,
115 weight_transfer_config=WeightTransferConfig(backend="nccl"),
116 - )
117 -
118 - world_size = llm.llm_engine.vllm_config.parallel_config.world_size
119 - assert world_size == 1
116 + ) as runner:
117 + world_size = runner.llm.llm_engine.vllm_config.parallel_config.world_size
118 + assert world_size == 1
119
120
121 @create_new_process_for_each_test()
123 -def test_init_weight_transfer_engine_calls_engine():
122 +def test_init_weight_transfer_engine_calls_engine(vllm_runner):
123 """Test that init_weight_transfer_engine calls the engine's
124 init_transfer_engine method."""
125 if torch.accelerator.device_count() < 1:
@@ -131,17 +130,20 @@ def test_init_weight_transfer_engine_calls_engine():
130 # Enable insecure serialization to allow pickling functions for collective_rpc
131 os.environ["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
132
134 - with patch(
135 - "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
136 - mock_create_engine,
137 - ):
138 - llm = LLM(
139 - model=MODEL_NAME,
133 + with (
134 + patch(
135 + "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
136 + mock_create_engine,
137 + ),
138 + vllm_runner(
139 + MODEL_NAME,
140 enforce_eager=True,
141 load_format="dummy",
142 tensor_parallel_size=1,
143 weight_transfer_config=WeightTransferConfig(backend="nccl"),
144 - )
144 + ) as runner,
145 + ):
146 + llm = weakref.proxy(runner.llm)
147
148 # Verify engine was created
149 def check_engine_exists(self):
@@ -170,7 +172,7 @@ def test_init_weight_transfer_engine_calls_engine():
172
173
174 @create_new_process_for_each_test()
173 -def test_update_weights_calls_engine():
175 +def test_update_weights_calls_engine(vllm_runner):
176 """Test that update_weights calls the engine's receive_weights method."""
177 if torch.accelerator.device_count() < 1:
178 pytest.skip("Need at least 1 GPU for this test")
@@ -180,17 +182,20 @@ def test_update_weights_calls_engine():
182 # Enable insecure serialization to allow pickling functions for collective_rpc
183 os.environ["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
184
183 - with patch(
184 - "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
185 - mock_create_engine,
186 - ):
187 - llm = LLM(
188 - model=MODEL_NAME,
185 + with (
186 + patch(
187 + "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
188 + mock_create_engine,
189 + ),
190 + vllm_runner(
191 + MODEL_NAME,
192 enforce_eager=True,
193 load_format="dummy",
194 tensor_parallel_size=1,
195 weight_transfer_config=WeightTransferConfig(backend="nccl"),
193 - )
196 + ) as runner,
197 + ):
198 + llm = weakref.proxy(runner.llm)
199
200 # First init the weight transfer
201 llm.init_weight_transfer_engine(
@@ -233,7 +238,7 @@ def test_update_weights_calls_engine():
238
239
240 @create_new_process_for_each_test()
236 -def test_full_weight_transfer_flow():
241 +def test_full_weight_transfer_flow(vllm_runner):
242 """Test the complete weight transfer flow: init -> start -> update -> finish."""
243 if torch.accelerator.device_count() < 1:
244 pytest.skip("Need at least 1 GPU for this test")
@@ -243,17 +248,20 @@ def test_full_weight_transfer_flow():
248 # Enable insecure serialization to allow pickling functions for collective_rpc
249 os.environ["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
250
246 - with patch(
247 - "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
248 - mock_create_engine,
249 - ):
250 - llm = LLM(
251 - model=MODEL_NAME,
251 + with (
252 + patch(
253 + "vllm.v1.worker.gpu_worker.WeightTransferEngineFactory.create_engine",
254 + mock_create_engine,
255 + ),
256 + vllm_runner(
257 + MODEL_NAME,
258 enforce_eager=True,
259 load_format="dummy",
260 tensor_parallel_size=1,
261 weight_transfer_config=WeightTransferConfig(backend="nccl"),
256 - )
262 + ) as runner,
263 + ):
264 + llm = weakref.proxy(runner.llm)
265
266 assert llm.get_weight_version() == "default"
267
@@ -309,20 +317,19 @@ def test_full_weight_transfer_flow():
317
318
319 @create_new_process_for_each_test()
312 -def test_weight_transfer_config_backend():
320 +def test_weight_transfer_config_backend(vllm_runner):
321 """Test that WeightTransferConfig backend is properly configured."""
322 if torch.accelerator.device_count() < 1:
323 pytest.skip("Need at least 1 GPU for this test")
324
325 # Test with nccl backend
318 - llm = LLM(
319 - model=MODEL_NAME,
326 + with vllm_runner(
327 + MODEL_NAME,
328 enforce_eager=True,
329 load_format="dummy",
330 tensor_parallel_size=1,
331 weight_transfer_config=WeightTransferConfig(backend="nccl"),
324 - )
325 -
326 - config = llm.llm_engine.vllm_config.weight_transfer_config
327 - assert config is not None
328 - assert config.backend == "nccl"
332 + ) as runner:
333 + config = runner.llm.llm_engine.vllm_config.weight_transfer_config
334 + assert config is not None
335 + assert config.backend == "nccl"