From 100e1cd0d9d5021c9f991b152e6852f8c2e1278e Mon Sep 17 00:00:00 2001 From: Dayananda V Date: Wed, 16 Sep 2026 10:14:09 +0530 Subject: [PATCH] [Fix] Spawn, don't fork, the benchmark server process (#34712) Co-authored-by: Claude Opus 5 --- python/sglang/benchmark/endpoint.py | 14 ++--- .../unit/bench/test_benchmark_endpoint.py | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 test/registered/unit/bench/test_benchmark_endpoint.py diff --git a/python/sglang/benchmark/endpoint.py b/python/sglang/benchmark/endpoint.py index 44c0b63e6..62e893221 100644 --- a/python/sglang/benchmark/endpoint.py +++ b/python/sglang/benchmark/endpoint.py @@ -50,14 +50,13 @@ def _launch_server_target(launch_server_func: Callable, server_args: ServerArgs) def launch_or_reuse_server(launch_server_func: Callable, server_args: ServerArgs): - # Resolve in the parent, before the fork. The pipeline probes the device - # (the default attention backend reads the CUDA capability), and a forked - # child cannot re-initialize CUDA once this process has. + # Resolving probes the device: the default attention backend reads the CUDA + # capability, and XPU reads mem_get_info. This process owns a live context after. server_args.resolve_once() base_url = resolve_base_url("", server_args.host, server_args.port) - # Reuse an already-running server instead of forking a second one onto the + # Reuse an already-running server instead of launching a second one onto the # occupied port, where it would orphan, compete for the GPU, and OOM. if server_is_up(base_url, timeout=5): print( @@ -66,7 +65,9 @@ def launch_or_reuse_server(launch_server_func: Callable, server_args: ServerArgs ) return None, base_url - proc = multiprocessing.Process( + # Spawn, not the platform default: a fork inherits the context resolve_once() + # initialized above, and CUDA/XPU cannot be re-initialized in a forked child. + proc = multiprocessing.get_context("spawn").Process( target=_launch_server_target, args=( launch_server_func, @@ -99,7 +100,8 @@ class BenchEndpoint: """ base_url: str - _proc: Optional[multiprocessing.Process] = None + # SpawnProcess is a sibling of multiprocessing.Process, not a subclass. + _proc: Optional[multiprocessing.process.BaseProcess] = None def close(self) -> None: if self._proc is not None: diff --git a/test/registered/unit/bench/test_benchmark_endpoint.py b/test/registered/unit/bench/test_benchmark_endpoint.py new file mode 100644 index 000000000..61e39899b --- /dev/null +++ b/test/registered/unit/bench/test_benchmark_endpoint.py @@ -0,0 +1,53 @@ +"""Unit tests for sglang/benchmark/endpoint.py""" + +import multiprocessing +import unittest +from unittest import mock + +from sglang.benchmark.endpoint import launch_or_reuse_server +from sglang.test.ci.ci_register import register_cpu_ci +from sglang.test.test_utils import CustomTestCase + +register_cpu_ci(est_time=10, suite="base-a-test-cpu") + + +def _noop_launch_server(server_args): + """Module-level, not a lambda: spawn pickles ``target`` at Process.start().""" + + +class _StubServerArgs: + """resolve_once() is where a real record probes the device; stubbing it is what + keeps this on CPU. Nothing but host/port is read before the child is created.""" + + host = "127.0.0.1" + port = 30000 + + def resolve_once(self): + pass + + +class TestLaunchOrReuseServer(CustomTestCase): + def test_server_child_is_a_spawn_process(self): + """#34709: the child must be spawned -- a fork inherits the accelerator + context resolve_once() left in the parent and dies on its first device op. + """ + with ( + # Patch BaseProcess, not get_context: mocking the context away would + # erase the start method under test. Only the launch is suppressed. + mock.patch.object(multiprocessing.process.BaseProcess, "start"), + mock.patch.object( + multiprocessing.process.BaseProcess, "is_alive", return_value=True + ), + # Down for the reuse probe, up on the first startup poll; extend the + # list if the startup loop ever gains another probe. + mock.patch( + "sglang.benchmark.endpoint.server_is_up", side_effect=[False, True] + ), + ): + proc, _ = launch_or_reuse_server(_noop_launch_server, _StubServerArgs()) + + self.assertIsInstance(proc, multiprocessing.context.SpawnProcess) + + +if __name__ == "__main__": + unittest.main()