diff --git a/tests/utils_/test_system_utils.py b/tests/utils_/test_system_utils.py index 5ef55877a7c7..dba31184920c 100644 --- a/tests/utils_/test_system_utils.py +++ b/tests/utils_/test_system_utils.py @@ -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" + diff --git a/vllm/entrypoints/openai/api_server.py b/vllm/entrypoints/openai/api_server.py index e1e2ef72bbdb..fbbc1e6ab85d 100644 --- a/vllm/entrypoints/openai/api_server.py +++ b/vllm/entrypoints/openai/api_server.py @@ -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!") diff --git a/vllm/envs.py b/vllm/envs.py index 265477ea7b93..e5a9ea581369 100755 --- a/vllm/envs.py +++ b/vllm/envs.py @@ -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 @@ -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( diff --git a/vllm/utils/system_utils.py b/vllm/utils/system_utils.py index 7f56f972a4fa..461a5fabcf83 100644 --- a/vllm/utils/system_utils.py +++ b/vllm/utils/system_utils.py @@ -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 = []