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

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

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

Synced Nov 14, 05:30 UTC

[CI] Fail subprocess tests with root-cause error (#23795)

Signed-off-by: Nick Hill <nhill@redhat.com>

Nick Hill committed Sep 10, 2025 at 13:53 UTC 4db44264047c2d30d9ba4eac55acaef821ae18b8
6 files changed +138 -33
requirements/test.in
+1
@@ -21,6 +21,7 @@ ray[cgraph,default]>=2.48.0 # Ray Compiled Graph, required by pipeline paralleli
21 sentence-transformers # required for embedding tests
22 soundfile # required for audio tests
23 jiwer # required for audio tests
24 +tblib # for pickling test exceptions
25 timm >=1.0.17 # required for internvl and gemma3n-mm test
26 torch==2.8.0
27 torchaudio==2.8.0
requirements/test.txt
+3 -1
@@ -137,7 +137,7 @@ contourpy==1.3.0
137 # via matplotlib
138 cramjam==2.9.0
139 # via fastparquet
140 -cupy-cuda12x==13.3.0
140 +cupy-cuda12x==13.6.0
141 # via ray
142 cycler==0.12.1
143 # via matplotlib
@@ -1032,6 +1032,8 @@ tabledata==1.3.3
1032 # via pytablewriter
1033 tabulate==0.9.0
1034 # via sacrebleu
1035 +tblib==3.1.0
1036 + # via -r requirements/test.in
1037 tcolorpy==0.1.6
1038 # via pytablewriter
1039 tenacity==9.0.0
tests/async_engine/test_api_server.py
+26
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +import copyreg
5 import os
6 import subprocess
7 import sys
@@ -10,6 +11,30 @@ from pathlib import Path
11
12 import pytest
13 import requests
14 +import urllib3.exceptions
15 +
16 +
17 +def _pickle_new_connection_error(obj):
18 + """Custom pickler for NewConnectionError to fix tblib compatibility."""
19 + # Extract the original message by removing the "conn: " prefix
20 + full_message = obj.args[0] if obj.args else ""
21 + if ': ' in full_message:
22 + # Split off the connection part and keep the actual message
23 + _, actual_message = full_message.split(': ', 1)
24 + else:
25 + actual_message = full_message
26 + return _unpickle_new_connection_error, (actual_message, )
27 +
28 +
29 +def _unpickle_new_connection_error(message):
30 + """Custom unpickler for NewConnectionError."""
31 + # Create with None as conn and the actual message
32 + return urllib3.exceptions.NewConnectionError(None, message)
33 +
34 +
35 +# Register the custom pickle/unpickle functions for tblib compatibility
36 +copyreg.pickle(urllib3.exceptions.NewConnectionError,
37 + _pickle_new_connection_error)
38
39
40 def _query_server(prompt: str, max_tokens: int = 5) -> dict:
@@ -52,6 +77,7 @@ def api_server(distributed_executor_backend: str):
77 uvicorn_process.terminate()
78
79
80 +@pytest.mark.timeout(300)
81 @pytest.mark.parametrize("distributed_executor_backend", ["mp", "ray"])
82 def test_api_server(api_server, distributed_executor_backend: str):
83 """
tests/conftest.py
+10
@@ -1,5 +1,15 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# ruff: noqa
5 +
6 +from tblib import pickling_support
7 +
8 +# Install support for pickling exceptions so that we can nicely propagate
9 +# failures from tests running in a subprocess.
10 +# This should be run before any custom exception subclasses are defined.
11 +pickling_support.install()
12 +
13 import http.server
14 import json
15 import math
tests/utils.py
+92 -28
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import asyncio
5 +import contextlib
6 import copy
7 import functools
8 import importlib
@@ -13,7 +14,7 @@ import sys
14 import tempfile
15 import time
16 import warnings
16 -from contextlib import contextmanager, suppress
17 +from contextlib import ExitStack, contextmanager, suppress
18 from multiprocessing import Process
19 from pathlib import Path
20 from typing import Any, Callable, Literal, Optional, Union
@@ -800,43 +801,106 @@ _P = ParamSpec("_P")
801
802
803 def fork_new_process_for_each_test(
803 - f: Callable[_P, None]) -> Callable[_P, None]:
804 + func: Callable[_P, None]) -> Callable[_P, None]:
805 """Decorator to fork a new process for each test function.
806 See https://github.com/vllm-project/vllm/issues/7053 for more details.
807 """
808
808 - @functools.wraps(f)
809 + @functools.wraps(func)
810 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None:
811 # Make the process the leader of its own process group
812 # to avoid sending SIGTERM to the parent process
813 os.setpgrp()
814 from _pytest.outcomes import Skipped
814 - pid = os.fork()
815 - print(f"Fork a new process to run a test {pid}")
816 - if pid == 0:
817 - try:
818 - f(*args, **kwargs)
819 - except Skipped as e:
820 - # convert Skipped to exit code 0
821 - print(str(e))
822 - os._exit(0)
823 - except Exception:
824 - import traceback
825 - traceback.print_exc()
826 - os._exit(1)
815 +
816 + # Create a unique temporary file to store exception info from child
817 + # process. Use test function name and process ID to avoid collisions.
818 + with tempfile.NamedTemporaryFile(
819 + delete=False,
820 + mode='w+b',
821 + prefix=f"vllm_test_{func.__name__}_{os.getpid()}_",
822 + suffix=".exc") as exc_file, ExitStack() as delete_after:
823 + exc_file_path = exc_file.name
824 + delete_after.callback(os.remove, exc_file_path)
825 +
826 + pid = os.fork()
827 + print(f"Fork a new process to run a test {pid}")
828 + if pid == 0:
829 + # Parent process responsible for deleting, don't delete
830 + # in child.
831 + delete_after.pop_all()
832 + try:
833 + func(*args, **kwargs)
834 + except Skipped as e:
835 + # convert Skipped to exit code 0
836 + print(str(e))
837 + os._exit(0)
838 + except Exception as e:
839 + import traceback
840 + tb_string = traceback.format_exc()
841 +
842 + # Try to serialize the exception object first
843 + exc_to_serialize: dict[str, Any]
844 + try:
845 + # First, try to pickle the actual exception with
846 + # its traceback.
847 + exc_to_serialize = {'pickled_exception': e}
848 + # Test if it can be pickled
849 + cloudpickle.dumps(exc_to_serialize)
850 + except (Exception, KeyboardInterrupt):
851 + # Fall back to string-based approach.
852 + exc_to_serialize = {
853 + 'exception_type': type(e).__name__,
854 + 'exception_msg': str(e),
855 + 'traceback': tb_string,
856 + }
857 + try:
858 + with open(exc_file_path, 'wb') as f:
859 + cloudpickle.dump(exc_to_serialize, f)
860 + except Exception:
861 + # Fallback: just print the traceback.
862 + print(tb_string)
863 + os._exit(1)
864 + else:
865 + os._exit(0)
866 else:
828 - os._exit(0)
829 - else:
830 - pgid = os.getpgid(pid)
831 - _pid, _exitcode = os.waitpid(pid, 0)
832 - # ignore SIGTERM signal itself
833 - old_signal_handler = signal.signal(signal.SIGTERM, signal.SIG_IGN)
834 - # kill all child processes
835 - os.killpg(pgid, signal.SIGTERM)
836 - # restore the signal handler
837 - signal.signal(signal.SIGTERM, old_signal_handler)
838 - assert _exitcode == 0, (f"function {f} failed when called with"
839 - f" args {args} and kwargs {kwargs}")
867 + pgid = os.getpgid(pid)
868 + _pid, _exitcode = os.waitpid(pid, 0)
869 + # ignore SIGTERM signal itself
870 + old_signal_handler = signal.signal(signal.SIGTERM,
871 + signal.SIG_IGN)
872 + # kill all child processes
873 + os.killpg(pgid, signal.SIGTERM)
874 + # restore the signal handler
875 + signal.signal(signal.SIGTERM, old_signal_handler)
876 + if _exitcode != 0:
877 + # Try to read the exception from the child process
878 + exc_info = {}
879 + if os.path.exists(exc_file_path):
880 + with contextlib.suppress(Exception), \
881 + open(exc_file_path, 'rb') as f:
882 + exc_info = cloudpickle.load(f)
883 +
884 + if (original_exception :=
885 + exc_info.get('pickled_exception')) is not None:
886 + # Re-raise the actual exception object if it was
887 + # successfully pickled.
888 + assert isinstance(original_exception, Exception)
889 + raise original_exception
890 +
891 + if (original_tb := exc_info.get("traceback")) is not None:
892 + # Use string-based traceback for fallback case
893 + raise AssertionError(
894 + f"Test {func.__name__} failed when called with"
895 + f" args {args} and kwargs {kwargs}"
896 + f" (exit code: {_exitcode}):\n{original_tb}"
897 + ) from None
898 +
899 + # Fallback to the original generic error
900 + raise AssertionError(
901 + f"function {func.__name__} failed when called with"
902 + f" args {args} and kwargs {kwargs}"
903 + f" (exit code: {_exitcode})") from None
904
905 return wrapper
906
vllm/executor/ray_distributed_executor.py
+6 -4
@@ -117,10 +117,12 @@ class RayDistributedExecutor(DistributedExecutorBase):
117 self.driver_worker.execute_method)
118
119 def shutdown(self) -> None:
120 - logger.info(
121 - "Shutting down Ray distributed executor. If you see error log "
122 - "from logging.cc regarding SIGTERM received, please ignore because "
123 - "this is the expected termination process in Ray.")
120 + if logger:
121 + # Somehow logger can be None here.
122 + logger.info(
123 + "Shutting down Ray distributed executor. If you see error log "
124 + "from logging.cc regarding SIGTERM received, please ignore "
125 + "because this is the expected termination process in Ray.")
126 if hasattr(self, "forward_dag") and self.forward_dag is not None:
127 self.forward_dag.teardown()
128 import ray