【vllm】spawn
您提出了一个极其敏锐和关键的问题!这确实让我之前的解释变得不完整,因为它默认了 Unix 系统上最常见的fork行为。您贴出的代码显示了 vLLM 在特定情况下会使用spawn,这完全改变了资源传递的方式。
您是对的:在使用spawn模式时,子进程不会自动继承父进程的文件描述符。那么,共享套接字是如何实现的呢?
答案是:Python 的multiprocessing库为spawn模式提供了一套特殊的、底层的机制,用于在进程间安全地传递文件描述符这类“不可序列化”的资源。
这个过程比fork的隐式继承要复杂得多,但结果是相同的。让我们来揭开spawn模式下共享套接字的“魔法”。
spawn模式下的套接字“共享”步骤
当您使用spawn上下文启动一个进程,并试图将一个套接字对象sock作为参数传递时,multiprocessing库在后台执行了以下一系列操作:
第 1 步:父进程准备“交接” (Reduction)
- 当父进程调用
proc.start()时,multiprocessing库需要将args=(..., sock, ...)中的所有参数传递给新的子进程。 - 对于普通的可序列化对象(如字符串、数字),它会使用
pickle来序列化。 - 但
socket对象是不可序列化的。当multiprocessing遇到socket对象时,它会触发一个特殊的“归约” (reduction) 过程。
第 2 步:提取核心资源 - 文件描述符
- 这个“归约”过程的核心是调用
sock.fileno(),从 Python 的socket对象中提取出它在操作系统内核中对应的整数文件描述符 (file descriptor)。
第 3 步:通过 IPC 安全地跨进程传递文件描述符
- 这是最关键的一步。父进程需要将这个整数文件描述符传递给一个全新的、独立的子进程。
- 它通过一个底层的进程间通信 (IPC)机制来完成。在 Unix-like 系统上,这通常是通过Unix 域套接字 (Unix Domain Socket)和一种叫做辅助数据 (ancillary data)的特殊消息 (
SCM_RIGHTS) 来实现的。 - 可以把它想象成:父进程建立了一条与即将出生的子进程之间的秘密通道。它不是把整个电话机(socket 对象)递过去,而是通过这个秘密通道,给了子进程一张写有“请接通 5 号线”(文件描述符)的特殊授权卡。
第 4 步:子进程“重建”套接字对象
- 子进程在启动时,其
multiprocessing初始化代码会通过那条秘密通道接收到父进程发送过来的文件描述符。 - 然后,它在子进程全新的内存空间里,创建一个新的 Python
socket对象。 - 最重要的是,它使用收到的文件描述符来初始化这个新的
socket对象(大致相当于socket.fromfd())。
最终结果
最终结果是:虽然子进程的内存空间是全新的,但它内部的sock对象和父进程的sock对象,都指向了内核中同一个、唯一的监听套接字数据结构。
所以,尽管实现方式天差地别:
fork: 通过内存继承实现了共享。spawn: 通过显式的、基于 IPC 的文件描述符传递和对象重建实现了共享。
但它们都达到了相同的目的:多个进程最终都在操作同一个内核监听队列。因此,我们之前讨论的关于内核级负载均衡、避免用户态锁开销、解决惊群效应等所有优点,在这种模式下依然完全适用。
为什么 vLLM 会使用spawn?
既然fork这么简单直接,为什么还要用spawn这么复杂的方式呢?
安全性与隔离性:
spawn更“干净”。子进程从一个纯净的环境开始,不会继承父进程可能存在的混乱状态(比如线程锁、全局变量等)。对于使用了复杂第三方库(尤其是那些非“fork-safe”的库)的程序来说,spawn更安全、更可预测。跨平台兼容性:
spawn是 Windows 和 macOS(新版本)上默认或唯一支持的多进程启动方式。使用spawn可以让代码更具可移植性。避免 CUDA 问题: 这是最常见的一个原因。CUDA 运行时与
fork存在已知的兼容性问题。如果在父进程初始化 CUDA 之后再fork子进程,很可能会导致子进程中的 CUDA 调用挂起或直接崩溃。因此,在涉及 GPU 的应用中,强制使用spawn是一种常见的、稳健的做法。get_mp_context()函数中的_maybe_force_spawn()正是做了这个判断。
总结:
您发现的spawn用法非常正确!它揭示了 vLLM 为了健壮性和兼容性(特别是与 GPU 环境的兼容性)而采用的更复杂的进程创建策略。虽然实现机制从“隐式继承”变成了“显式传递”,但“多个进程共享同一个内核套接字”的最终效果没有改变,其性能优势也依然存在。感谢您提出这个精彩的问题,它让我们的讨论更加深入和准确!
不是,vLLM 不是通过父进程 fork() 创建 API server 进程的。vLLM 使用的是spawn方法来创建多个 API server 进程。 1
具体实现
在APIServerProcessManager类中,vLLM 明确使用 spawn 上下文创建进程:
# Start API serversspawn_context=multiprocessing.get_context("spawn")self.processes:list[BaseProcess]=[]fori,in_addr,out_addrinzip(range(num_servers),input_addresses,output_addresses):proc=spawn_context.Process(target=target_server_fn,name=f"ApiServer_{i}",args=(listen_address,sock,args,client_config),)self.processes.append(proc)proc.start()```[1](#7-0)## 为什么使用 spawn 而不是 forkvLLM 选择 spawn 方法主要有以下原因:1.**兼容性更好**:spawn 与更多依赖库兼容,特别是那些使用线程的库[2](#7-1)2.**避免 CUDA 初始化问题**:fork 在 CUDA 已初始化后会有问题,spawn 更安全[3](#7-2)3.**环境变量控制**:虽然默认的 `VLLM_WORKER_MULTIPROC_METHOD` 是"fork"[4](#7-3) ,但在 API server 创建时强制使用 spawn。## 进程创建流程1.`run_multi_api_server` 函数启动[5](#7-4)2.创建 `APIServerProcessManager` 实例3.使用 spawn 上下文创建多个 API server 工作进程4.每个进程独立运行,共享监听 socket## Notes虽然 fork 方法更快(因为它继承父进程的内存空间),但 spawn 方法更安全可靠。vLLM 在 API server 创建时选择牺牲一些性能来换取更好的稳定性和兼容性。这种设计确保了多进程架构在各种环境下都能正常工作。[6](#7-5)Wiki pages you might want to explore:-[ConfigurationandInitialization(vllm-project/vllm)](/wiki/vllm-project/vllm#2)### Citations**File:**vllm/v1/utils.py(L194-215)```python spawn_context=multiprocessing.get_context("spawn")self.processes:list[BaseProcess]=[]fori,in_addr,out_addrinzip(range(num_servers),input_addresses,output_addresses):client_config={"input_address":in_addr,"output_address":out_addr,"client_count":num_servers,"client_index":i,}ifstats_update_addressisnotNone:client_config["stats_update_address"]=stats_update_address proc=spawn_context.Process(target=target_server_fn,name=f"ApiServer_{i}",args=(listen_address,sock,args,client_config),)self.processes.append(proc)proc.start()File:docs/design/multiprocessing.md (L35-41)
`fork` is the fastest method, but is incompatible with dependencies that use threads. If you are under macOS, using `fork` may cause the process to crash. `spawn` is more compatible with dependencies, but can be problematic when vLLM is used as a library. If the consuming code does not use a `__main__` guard (`if __name__ == "__main__":`), the code will be inadvertently re-executed when vLLM spawns a new process. This can lead to infinite recursion, among other problems.File:docs/design/multiprocessing.md (L60-62)
It is perhaps more accurate to say that there are known problems with using `fork` after initializing these dependencies.File:docs/design/multiprocessing.md (L87-114)
## Prior State in v1 There was an environment variable to control whether multiprocessing is used in the v1 engine core, `VLLM_ENABLE_V1_MULTIPROCESSING`. This defaulted to off. - <https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/envs.py#L452-L454> When it was enabled, the v1 `LLMEngine` would create a new process to run the engine core. - <https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/v1/engine/llm_engine.py#L93-L95> - <https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/v1/engine/llm_engine.py#L70-L77> - <https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/v1/engine/core_client.py#L44-L45> It was off by default for all the reasons mentioned above - compatibility with dependencies and code using vLLM as a library. ### Changes Made in v1 There is not an easy solution with Python's `multiprocessing` that will work everywhere. As a first step, we can get v1 into a state where it does "best effort" choice of multiprocessing method to maximize compatibility. - Default to `fork`. - Use `spawn` when we know we control the main process (`vllm` was executed). - If we detect `cuda` was previously initialized, force `spawn` and emit a warning. We know `fork` will break, so this is the best we can do.File:vllm/envs.py (L728-730)
"VLLM_WORKER_MULTIPROC_METHOD":env_with_choices("VLLM_WORKER_MULTIPROC_METHOD","fork",["spawn","fork"]),File:vllm/entrypoints/cli/serve.py (L218-288)
defrun_multi_api_server(args:argparse.Namespace):assertnotargs.headless num_api_servers:int=args.api_server_countassertnum_api_servers>0ifnum_api_servers>1:setup_multiprocess_prometheus()listen_address,sock=setup_server(args)engine_args=vllm.AsyncEngineArgs.from_cli_args(args)engine_args._api_process_count=num_api_servers engine_args._api_process_rank=-1usage_context=UsageContext.OPENAI_API_SERVER vllm_config=engine_args.create_engine_config(usage_context=usage_context)ifnum_api_servers>1andenvs.VLLM_ALLOW_RUNTIME_LORA_UPDATING:raiseValueError("VLLM_ALLOW_RUNTIME_LORA_UPDATING cannot be used with api_server_count > 1")executor_class=Executor.get_class(vllm_config)log_stats=notengine_args.disable_log_stats parallel_config=vllm_config.parallel_config dp_rank=parallel_config.data_parallel_rankassertparallel_config.local_engines_onlyordp_rank==0api_server_manager:APIServerProcessManager|None=Nonewithlaunch_core_engines(vllm_config,executor_class,log_stats,num_api_servers)as(local_engine_manager,coordinator,addresses):# Construct common args for the APIServerProcessManager up-front.api_server_manager_kwargs=dict(target_server_fn=run_api_server_worker_proc,listen_address=listen_address,sock=sock,args=args,num_servers=num_api_servers,input_addresses=addresses.inputs,output_addresses=addresses.outputs,stats_update_address=coordinator.get_stats_publish_address()ifcoordinatorelseNone,)# For dp ranks > 0 in external/hybrid DP LB modes, we must delay the# start of the API servers until the local engine is started# (after the launcher context manager exits),# since we get the front-end stats update address from the coordinator# via the handshake with the local engine.ifdp_rank==0ornotparallel_config.local_engines_only:# Start API servers using the manager.api_server_manager=APIServerProcessManager(**api_server_manager_kwargs)# Start API servers now if they weren't already started.ifapi_server_managerisNone:api_server_manager_kwargs["stats_update_address"]=(addresses.frontend_stats_publish_address)api_server_manager=APIServerProcessManager(**api_server_manager_kwargs)# Wait for API serverswait_for_completion_or_failure(api_server_manager=api_server_manager,engine_manager=local_engine_manager,coordinator=coordinator,)