Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions tests/utils_/test_system_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,49 @@ def test_numa_bind_forces_spawn(monkeypatch):
monkeypatch.setattr("sys.argv", ["vllm", "serve", "--numa-bind"])
_maybe_force_spawn()
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "spawn"

def test_forkserver_opt_in_survives_cuda_init(monkeypatch):
"""Regression test: when the user explicitly opts into
VLLM_WORKER_MULTIPROC_METHOD=forkserver, _maybe_force_spawn must NOT
silently rewrite it to "spawn" — even when CUDA is initialized in the
parent. The forkserver helper process forks workers from a clean snapshot
taken before CUDA init, so the CUDA-init hazard that motivates forcing
spawn does not apply."""
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "forkserver")
monkeypatch.setattr("sys.argv", ["vllm", "serve"])
# Simulate CUDA initialized in the parent process — without the
# forkserver early-return, _maybe_force_spawn would rewrite to "spawn".
monkeypatch.setattr(
"vllm.utils.system_utils.cuda_is_initialized", lambda: True
)
_maybe_force_spawn()
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "forkserver"


def test_forkserver_opt_in_survives_numa_bind(monkeypatch):
"""Same contract under --numa-bind: forkserver opt-in is preserved."""
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "forkserver")
monkeypatch.setattr("sys.argv", ["vllm", "serve", "--numa-bind"])
_maybe_force_spawn()
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "forkserver"


def test_spawn_opt_in_still_short_circuits(monkeypatch):
"""Sanity: existing "spawn" early-return still works (regression guard
against accidentally clobbering the original branch)."""
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
monkeypatch.setattr("sys.argv", ["vllm", "serve", "--numa-bind"])
_maybe_force_spawn()
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "spawn"


def test_unset_with_cuda_init_still_forces_spawn(monkeypatch):
"""When the user has NOT opted in, CUDA-init still forces spawn."""
monkeypatch.delenv("VLLM_WORKER_MULTIPROC_METHOD", raising=False)
monkeypatch.setattr("sys.argv", ["vllm", "serve"])
monkeypatch.setattr(
"vllm.utils.system_utils.cuda_is_initialized", lambda: True
)
_maybe_force_spawn()
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "spawn"

21 changes: 16 additions & 5 deletions vllm/entrypoints/openai/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,23 @@ async def build_async_engine_client(
usage_context: UsageContext = UsageContext.OPENAI_API_SERVER,
client_config: dict[str, Any] | None = None,
) -> AsyncIterator[EngineClient]:
if os.getenv("VLLM_WORKER_MULTIPROC_METHOD") == "forkserver":
if envs.VLLM_WORKER_MULTIPROC_METHOD == "forkserver":
# The executor is expected to be mp.
# Pre-import heavy modules in the forkserver process
logger.debug("Setup forkserver with pre-imports")
multiprocessing.set_start_method("forkserver")
multiprocessing.set_forkserver_preload(["vllm.v1.engine.async_llm"])
# Start the forkserver helper process eagerly so it forks workers
# from a clean parent (before CUDA/heavy imports). force=True so
# re-entry (e.g. test fixtures that already set a method) does not
# raise RuntimeError.
#
# Note: we intentionally do NOT call set_forkserver_preload() here.
# The original landing of forkserver support (PR #40331) was reverted
# in PR #40438 because preloading vllm.v1.engine.async_llm pulled in
# a background-thread "import transformers" that broke the pooling
# tests. The eager-import optimization is fully separable from the
# forkserver opt-in; forkserver still works without it (cold imports
# per fork), and re-introducing the preload would re-introduce the
# original regression.
logger.debug("Setup forkserver")
multiprocessing.set_start_method("forkserver", force=True)
forkserver.ensure_running()
logger.debug("Forkserver setup complete!")

Expand Down
4 changes: 2 additions & 2 deletions vllm/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
VLLM_USE_RAY_V2_EXECUTOR_BACKEND: bool = False
VLLM_DISTRIBUTED_USE_SPLIT_GROUP: bool = False
VLLM_XLA_USE_SPMD: bool = False
VLLM_WORKER_MULTIPROC_METHOD: Literal["fork", "spawn"] = "fork"
VLLM_WORKER_MULTIPROC_METHOD: Literal["fork", "spawn", "forkserver"] = "fork"
VLLM_ASSETS_CACHE: str = os.path.join(VLLM_CACHE_ROOT, "assets")
VLLM_ASSETS_CACHE_MODEL_CLEAN: bool = False
VLLM_IMAGE_FETCH_TIMEOUT: int = 5
Expand Down Expand Up @@ -861,7 +861,7 @@ def _resolve_rust_frontend_path() -> str | None:
# Use dedicated multiprocess context for workers.
# Both spawn and fork work
"VLLM_WORKER_MULTIPROC_METHOD": env_with_choices(
"VLLM_WORKER_MULTIPROC_METHOD", "fork", ["spawn", "fork"]
"VLLM_WORKER_MULTIPROC_METHOD", "fork", ["spawn", "fork", "forkserver"]
),
# Path to the cache for storing downloaded assets
"VLLM_ASSETS_CACHE": lambda: os.path.expanduser(
Expand Down
10 changes: 9 additions & 1 deletion vllm/utils/system_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,16 @@ def _sync_visible_devices_env_vars():
def _maybe_force_spawn():
"""Check if we need to force the use of the `spawn` multiprocessing start
method.

If the user has explicitly opted into "spawn" or "forkserver", we skip the
force-spawn override. forkserver is only enabled when the parent has not
yet initialized CUDA (the api_server entrypoint pre-starts the forkserver
via forkserver.ensure_running() before any CUDA touch), so the
CUDA-init / Ray-actor / WSL / NUMA-bind hazards that motivate forcing
spawn do not apply, and silently rewriting forkserver to spawn would
defeat the user opt-in.
"""
if os.environ.get("VLLM_WORKER_MULTIPROC_METHOD") == "spawn":
if os.environ.get("VLLM_WORKER_MULTIPROC_METHOD") in ("spawn", "forkserver"):
return

reasons = []
Expand Down
Loading