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

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 10, 06:30 UTC

[ROCm][CI] Fix engine core client tests for ROCm spawn multiprocessing (#32061)

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

Andreas Karatzas committed Jan 13, 2026 at 01:14 UTC df7e12715fa5cac36069d3a58d869d9b939027a9
1 file changed +181 -70
tests/v1/engine/test_engine_core_client.py
+181 -70
@@ -3,6 +3,7 @@
3
4 import asyncio
5 import importlib
6 +import inspect
7 import os
8 import signal
9 import time
@@ -47,6 +48,7 @@ MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct"
48 TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME)
49 PROMPT = "Hello my name is Robert and I love quantization kernels"
50 PROMPT_TOKENS = TOKENIZER(PROMPT).input_ids
51 +TEST_MODULE = "tests.v1.engine.test_engine_core_client"
52
53 _REQUEST_COUNTER = 0
54
@@ -223,10 +225,170 @@ def echo(self, msg: str, err_msg: str | None = None, sleep: float | None = None)
225 return msg
226
227
228 +@dataclass
229 +class TestMessage:
230 + """Test dataclass for verifying custom type serialization."""
231 +
232 + message: str
233 +
234 +
235 +# Dummy utility function to monkey-patch into engine core.
236 +def echo_dc(
237 + self,
238 + msg: str,
239 + return_list: bool = False,
240 +) -> TestMessage | list[TestMessage]:
241 + print(f"echo dc util function called: {msg}")
242 + val = None if msg is None else TestMessage(msg)
243 + # Return dataclass to verify support for returning custom types
244 + # (for which there is special handling to make it work with msgspec).
245 + return [val for _ in range(3)] if return_list else val
246 +
247 +
248 +# Dummy utility function to test dict serialization with custom types.
249 +def echo_dc_dict(
250 + self,
251 + msg: str,
252 + return_dict: bool = False,
253 +) -> TestMessage | dict[str, TestMessage]:
254 + print(f"echo dc dict util function called: {msg}")
255 + val = None if msg is None else TestMessage(msg)
256 + # Return dict of dataclasses to verify support for returning dicts
257 + # with custom value types.
258 + if return_dict:
259 + return {"key1": val, "key2": val, "key3": val}
260 + else:
261 + return val
262 +
263 +
264 +# Dummy utility function to test nested structures with custom types.
265 +def echo_dc_nested(
266 + self,
267 + msg: str,
268 + structure_type: str = "list_of_dicts",
269 +) -> Any:
270 + print(f"echo dc nested util function called: {msg}, structure: {structure_type}")
271 + val = None if msg is None else TestMessage(msg)
272 +
273 + structures = {
274 + "list_of_dicts": [{"a": val, "b": val}, {"c": val, "d": val}],
275 + "dict_of_lists": {"list1": [val, val], "list2": [val, val]},
276 + "deep_nested": {"outer": [{"inner": [val, val]}, {"inner": [val]}]},
277 + }
278 + return structures.get(structure_type, val)
279 +
280 +
281 +# --- Fixtures for subprocess patching ---
282 +# These create sitecustomize.py files that patch EngineCore in spawned
283 +# subprocesses. This is necessary because ROCm requires 'spawn' multiprocessing
284 +# start method, which creates fresh Python interpreters that don't inherit
285 +# monkey-patches from the parent process.
286 +
287 +
288 +@pytest.fixture
289 +def subprocess_echo_patch(monkeypatch, tmp_path):
290 + """Create sitecustomize.py so spawned subprocesses have echo method.
291 +
292 + This is needed because ROCm uses 'spawn' multiprocessing start method,
293 + which creates a fresh Python interpreter that doesn't inherit monkey-patches.
294 + By using sitecustomize.py, we ensure the patch is applied when Python starts.
295 + """
296 + sc = tmp_path / "sitecustomize.py"
297 + sc.write_text(
298 + "\n".join(
299 + [
300 + "import time",
301 + "from vllm.v1.engine.core import EngineCore",
302 + inspect.getsource(echo),
303 + "EngineCore.echo = echo",
304 + ]
305 + )
306 + )
307 + monkeypatch.setenv(
308 + "PYTHONPATH",
309 + os.pathsep.join(filter(None, [str(tmp_path), os.getenv("PYTHONPATH")])),
310 + )
311 +
312 +
313 +@pytest.fixture
314 +def subprocess_echo_dc_patch(monkeypatch, tmp_path):
315 + """Create sitecustomize.py so spawned subprocesses have echo_dc method."""
316 + sc = tmp_path / "sitecustomize.py"
317 + sc.write_text(
318 + "\n".join(
319 + [
320 + "from dataclasses import dataclass",
321 + "",
322 + inspect.getsource(TestMessage),
323 + f"TestMessage.__module__ = '{TEST_MODULE}'",
324 + "",
325 + "from vllm.v1.engine.core import EngineCore",
326 + inspect.getsource(echo_dc),
327 + "EngineCore.echo_dc = echo_dc",
328 + ]
329 + )
330 + )
331 + monkeypatch.setenv(
332 + "PYTHONPATH",
333 + os.pathsep.join(filter(None, [str(tmp_path), os.getenv("PYTHONPATH")])),
334 + )
335 +
336 +
337 +@pytest.fixture
338 +def subprocess_echo_dc_dict_patch(monkeypatch, tmp_path):
339 + """Create sitecustomize.py so spawned subprocesses have echo_dc_dict method."""
340 + sc = tmp_path / "sitecustomize.py"
341 + sc.write_text(
342 + "\n".join(
343 + [
344 + "from dataclasses import dataclass",
345 + "",
346 + inspect.getsource(TestMessage),
347 + f"TestMessage.__module__ = '{TEST_MODULE}'",
348 + "",
349 + "from vllm.v1.engine.core import EngineCore",
350 + inspect.getsource(echo_dc_dict),
351 + "EngineCore.echo_dc_dict = echo_dc_dict",
352 + ]
353 + )
354 + )
355 + monkeypatch.setenv(
356 + "PYTHONPATH",
357 + os.pathsep.join(filter(None, [str(tmp_path), os.getenv("PYTHONPATH")])),
358 + )
359 +
360 +
361 +@pytest.fixture
362 +def subprocess_echo_dc_nested_patch(monkeypatch, tmp_path):
363 + """Create sitecustomize.py so spawned subprocesses have echo_dc_nested method."""
364 + sc = tmp_path / "sitecustomize.py"
365 + sc.write_text(
366 + "\n".join(
367 + [
368 + "from dataclasses import dataclass",
369 + "from typing import Any",
370 + "",
371 + inspect.getsource(TestMessage),
372 + f"TestMessage.__module__ = '{TEST_MODULE}'",
373 + "",
374 + "from vllm.v1.engine.core import EngineCore",
375 + inspect.getsource(echo_dc_nested),
376 + "EngineCore.echo_dc_nested = echo_dc_nested",
377 + ]
378 + )
379 + )
380 + monkeypatch.setenv(
381 + "PYTHONPATH",
382 + os.pathsep.join(filter(None, [str(tmp_path), os.getenv("PYTHONPATH")])),
383 + )
384 +
385 +
386 @create_new_process_for_each_test()
387 @pytest.mark.parametrize("multiprocessing_mode", [True, False])
388 def test_engine_core_client(
229 - monkeypatch: pytest.MonkeyPatch, multiprocessing_mode: bool
389 + monkeypatch: pytest.MonkeyPatch,
390 + multiprocessing_mode: bool,
391 + subprocess_echo_patch,
392 ):
393 with monkeypatch.context() as m:
394 # Monkey-patch core engine utility function to test.
@@ -313,7 +475,10 @@ def test_engine_core_client(
475
476
477 @pytest.mark.asyncio(loop_scope="function")
316 -async def test_engine_core_client_asyncio(monkeypatch: pytest.MonkeyPatch):
478 +async def test_engine_core_client_asyncio(
479 + monkeypatch: pytest.MonkeyPatch,
480 + subprocess_echo_patch,
481 +):
482 with monkeypatch.context() as m:
483 # Monkey-patch core engine utility function to test.
484 m.setattr(EngineCore, "echo", echo, raising=False)
@@ -406,66 +571,10 @@ async def test_engine_core_client_asyncio(monkeypatch: pytest.MonkeyPatch):
571 client.shutdown()
572
573
409 -@dataclass
410 -class MyDataclass:
411 - message: str
412 -
413 -
414 -# Dummy utility function to monkey-patch into engine core.
415 -def echo_dc(
416 - self,
417 - msg: str,
418 - return_list: bool = False,
419 -) -> MyDataclass | list[MyDataclass]:
420 - print(f"echo dc util function called: {msg}")
421 - val = None if msg is None else MyDataclass(msg)
422 - # Return dataclass to verify support for returning custom types
423 - # (for which there is special handling to make it work with msgspec).
424 - return [val for _ in range(3)] if return_list else val
425 -
426 -
427 -# Dummy utility function to test dict serialization with custom types.
428 -def echo_dc_dict(
429 - self,
430 - msg: str,
431 - return_dict: bool = False,
432 -) -> MyDataclass | dict[str, MyDataclass]:
433 - print(f"echo dc dict util function called: {msg}")
434 - val = None if msg is None else MyDataclass(msg)
435 - # Return dict of dataclasses to verify support for returning dicts
436 - # with custom value types.
437 - if return_dict:
438 - return {"key1": val, "key2": val, "key3": val}
439 - else:
440 - return val
441 -
442 -
443 -# Dummy utility function to test nested structures with custom types.
444 -def echo_dc_nested(
445 - self,
446 - msg: str,
447 - structure_type: str = "list_of_dicts",
448 -) -> Any:
449 - print(f"echo dc nested util function called: {msg}, structure: {structure_type}")
450 - val = None if msg is None else MyDataclass(msg)
451 -
452 - if structure_type == "list_of_dicts": # noqa
453 - # Return list of dicts: [{"a": val, "b": val}, {"c": val, "d": val}]
454 - return [{"a": val, "b": val}, {"c": val, "d": val}]
455 - elif structure_type == "dict_of_lists":
456 - # Return dict of lists: {"list1": [val, val], "list2": [val, val]}
457 - return {"list1": [val, val], "list2": [val, val]}
458 - elif structure_type == "deep_nested":
459 - # Return deeply nested: {"outer": [{"inner": [val, val]},
460 - # {"inner": [val]}]}
461 - return {"outer": [{"inner": [val, val]}, {"inner": [val]}]}
462 - else:
463 - return val
464 -
465 -
574 @pytest.mark.asyncio(loop_scope="function")
575 async def test_engine_core_client_util_method_custom_return(
576 monkeypatch: pytest.MonkeyPatch,
577 + subprocess_echo_dc_patch,
578 ):
579 with monkeypatch.context() as m:
580 # Must set insecure serialization to allow returning custom types.
@@ -494,10 +603,10 @@ async def test_engine_core_client_util_method_custom_return(
603 core_client: AsyncMPClient = client
604
605 result = await core_client.call_utility_async("echo_dc", "testarg2", False)
497 - assert isinstance(result, MyDataclass) and result.message == "testarg2"
606 + assert isinstance(result, TestMessage) and result.message == "testarg2"
607 result = await core_client.call_utility_async("echo_dc", "testarg2", True)
608 assert isinstance(result, list) and all(
500 - isinstance(r, MyDataclass) and r.message == "testarg2" for r in result
609 + isinstance(r, TestMessage) and r.message == "testarg2" for r in result
610 )
611
612 # Test returning None and list of Nones
@@ -513,6 +622,7 @@ async def test_engine_core_client_util_method_custom_return(
622 @pytest.mark.asyncio(loop_scope="function")
623 async def test_engine_core_client_util_method_custom_dict_return(
624 monkeypatch: pytest.MonkeyPatch,
625 + subprocess_echo_dc_dict_patch,
626 ):
627 with monkeypatch.context() as m:
628 # Must set insecure serialization to allow returning custom types.
@@ -544,7 +654,7 @@ async def test_engine_core_client_util_method_custom_dict_return(
654 result = await core_client.call_utility_async(
655 "echo_dc_dict", "testarg3", False
656 )
547 - assert isinstance(result, MyDataclass) and result.message == "testarg3"
657 + assert isinstance(result, TestMessage) and result.message == "testarg3"
658
659 # Test dict return with custom value types
660 result = await core_client.call_utility_async(
@@ -553,7 +663,7 @@ async def test_engine_core_client_util_method_custom_dict_return(
663 assert isinstance(result, dict) and len(result) == 3
664 for key, val in result.items():
665 assert key in ["key1", "key2", "key3"]
556 - assert isinstance(val, MyDataclass) and val.message == "testarg3"
666 + assert isinstance(val, TestMessage) and val.message == "testarg3"
667
668 # Test returning dict with None values
669 result = await core_client.call_utility_async("echo_dc_dict", None, True)
@@ -569,6 +679,7 @@ async def test_engine_core_client_util_method_custom_dict_return(
679 @pytest.mark.asyncio(loop_scope="function")
680 async def test_engine_core_client_util_method_nested_structures(
681 monkeypatch: pytest.MonkeyPatch,
682 + subprocess_echo_dc_nested_patch,
683 ):
684 with monkeypatch.context() as m:
685 # Must set insecure serialization to allow returning custom types.
@@ -605,21 +716,21 @@ async def test_engine_core_client_util_method_nested_structures(
716 if i == 0:
717 assert "a" in item and "b" in item
718 assert (
608 - isinstance(item["a"], MyDataclass)
719 + isinstance(item["a"], TestMessage)
720 and item["a"].message == "nested1"
721 )
722 assert (
612 - isinstance(item["b"], MyDataclass)
723 + isinstance(item["b"], TestMessage)
724 and item["b"].message == "nested1"
725 )
726 else:
727 assert "c" in item and "d" in item
728 assert (
618 - isinstance(item["c"], MyDataclass)
729 + isinstance(item["c"], TestMessage)
730 and item["c"].message == "nested1"
731 )
732 assert (
622 - isinstance(item["d"], MyDataclass)
733 + isinstance(item["d"], TestMessage)
734 and item["d"].message == "nested1"
735 )
736
@@ -632,7 +743,7 @@ async def test_engine_core_client_util_method_nested_structures(
743 for key, lst in result.items():
744 assert isinstance(lst, list) and len(lst) == 2
745 for item in lst:
635 - assert isinstance(item, MyDataclass) and item.message == "nested2"
746 + assert isinstance(item, TestMessage) and item.message == "nested2"
747
748 # Test deeply nested: {"outer": [{"inner": [val, val]},
749 # {"inner": [val]}]}
@@ -649,7 +760,7 @@ async def test_engine_core_client_util_method_nested_structures(
760 inner_list1 = inner_dict1["inner"]
761 assert isinstance(inner_list1, list) and len(inner_list1) == 2
762 for item in inner_list1:
652 - assert isinstance(item, MyDataclass) and item.message == "nested3"
763 + assert isinstance(item, TestMessage) and item.message == "nested3"
764
765 # Second dict in outer list should have "inner" with 1 item
766 inner_dict2 = outer_list[1]
@@ -657,7 +768,7 @@ async def test_engine_core_client_util_method_nested_structures(
768 inner_list2 = inner_dict2["inner"]
769 assert isinstance(inner_list2, list) and len(inner_list2) == 1
770 assert (
660 - isinstance(inner_list2[0], MyDataclass)
771 + isinstance(inner_list2[0], TestMessage)
772 and inner_list2[0].message == "nested3"
773 )
774