[Fix] Spawn, don't fork, the benchmark server process (#34712)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dayananda V
2026-09-16 12:44:09 +08:00
committed by GitHub
co-authored by Claude Opus 5
parent 5f17e3a75f
commit 100e1cd0d9
2 changed files with 61 additions and 6 deletions
+8 -6
View File
@@ -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:
@@ -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()