[Fix] Spawn, don't fork, the benchmark server process (#34712)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f17e3a75f
commit
100e1cd0d9
@@ -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()
|
||||
Reference in New Issue
Block a user