
vLLM 中的 Python 多进程策略fork 与 spawn 的取舍、自动降级与故障排查【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllmvLLM 通过 Python 多进程来管理分布式 worker 和独立运行的引擎核心进程而多进程的启动方式fork/spawn/forkserver直接决定了它在 CUDA 初始化、Ray 环境、NUMA 绑定等场景下能否正常工作。本文基于仓库中的设计文档 docs/design/multiprocessing.md 展开结合 vllm/utils/system_utils.py、vllm/envs.py 等源码讲清 vLLM 如何为作为库使用与作为 CLI 运行两种场景自动选择多进程启动方式以及在出现问题时如何定位与修复。为什么 vLLM 的多进程使用很棘手vLLM 的多进程使用主要被两个因素复杂化引自设计文档vLLM 常被作为 Python 库使用调用方例如 Jupyter 脚本、Notebook、上层服务无法控制 vLLM 内部代码的执行环境与入口结构部分依赖库与某些多进程方法不兼容尤其是 PyTorch/CUDA 初始化之后fork子进程会继承父进程中已初始化的 CUDA 状态官方 PyTorch 文档明确建议 CUDA 场景使用spawn。这两点互相矛盾fork对库场景最友好但对 CUDA 不兼容spawn对 CUDA 友好但对缺少if __name__ __main__:保护的调用方代码不友好子进程会重新执行主模块代码可能引发无限递归等严重问题。vLLM 的设计目标就是在不要求用户改变使用习惯的前提下尽量让各种场景都能跑通。三种 Python 多进程启动方式与各自取舍Pythonmultiprocessing提供三种启动方式spawn启动一个全新的 Python 进程并重新导入执行主模块。Windows 和 macOS 的默认方式fork调用os.fork()复制当前解释器进程速度最快。Python 3.14 之前 Linux 的默认方式forkserver先启动一个服务器进程之后按需从服务器进程 fork 出子进程。Python 3.14 起成为 Linux 的默认方式。三者的取舍如下与设计文档一致fork最快但线程不兼容与使用了线程的依赖典型如已初始化的 CUDA Runtime不兼容在 macOS 上使用fork甚至可能导致进程崩溃。spawn兼容性更好但库场景有坑如果调用 vLLM 的代码没有写__main__保护spawn 出的子进程会重新执行整个主模块轻则重复初始化重则无限递归。forkserver表面最优实则与spawn同病服务器进程本身是以 spawn 方式创建的因此同样会重新执行未加__main__保护的主模块代码。另外对spawn和forkserver而言子进程不能依赖继承任何全局状态例如 fork 天然继承的父进程内存对象这一点在设计任何 worker 通信方案时都是硬约束。与依赖库的兼容性CUDA 之后不能 forkvLLM 的多个依赖PyTorch CUDA、Gaudi/ Habana 平台 PyTorch 等都明确表达了偏好或要求使用spawn的立场。因此在依赖库尤其是 CUDA已经初始化之后再fork子进程是已知会出问题的场景。这一点在当前仓库的源码中得到了直接印证。vllm/utils/system_utils.py 中的_maybe_force_spawn()会检查多种条件满足任一条件就强制把多进程方法覆盖为spawn并打印警告当前运行在 Ray actor 中is_in_ray_actor()此时还需把RAY_ADDRESS传给子进程才能连回集群命令行带--numa-bind参数NUMA 绑定依赖可执行文件劫持需要 spawnCUDA 已经被初始化cuda_is_initialized()XPU 已经被初始化xpu_is_initialized()检测到 WSL 环境NVML 与 fork 不兼容。核心逻辑如下def _maybe_force_spawn(): Check if we need to force the use of the spawn multiprocessing start method. if os.environ.get(VLLM_WORKER_MULTIPROC_METHOD) spawn: return reasons [] if is_in_ray_actor(): ... reasons.append(In a Ray actor and can only be spawned) if --numa-bind in sys.argv: reasons.append(NUMA binding requires spawn method) if cuda_is_initialized(): reasons.append(CUDA is initialized) elif xpu_is_initialized(): reasons.append(XPU is initialized) if in_wsl(): reasons.append(WSL is detected and NVML is not compatible with fork) if reasons: logger.warning( We must use the spawn multiprocessing start method. Overriding VLLM_WORKER_MULTIPROC_METHOD to spawn. ...) os.environ[VLLM_WORKER_MULTIPROC_METHOD] spawn也就是说vLLM 实现了设计文档中承诺的策略如果检测到cuda已被初始化强制spawn并发出警告。我们知道fork在这种情况下一定会坏这是我们能做到的最好方案。关键控制点VLLM_WORKER_MULTIPROC_METHOD环境变量环境变量定义与默认值在 vllm/envs.py 中该变量被声明为VLLM_WORKER_MULTIPROC_METHOD: Literal[fork, spawn] fork并在 vllm/envs.py 中注册为带取值约束的环境变量VLLM_WORKER_MULTIPROC_METHOD: env_with_choices( VLLM_WORKER_MULTIPROC_METHOD, fork, [spawn, fork] )即当前默认值为fork可选值为spawn或fork。所有 worker 进程的统一入口get_mp_context()vLLM 没有在各处散落地调用multiprocessing.get_context(...)而是收敛到一个函数 get_mp_context()def get_mp_context(): Get a multiprocessing context with a particular method (spawn or fork). By default we follow the value of the VLLM_WORKER_MULTIPROC_METHOD to determine the multiprocessing method (default is fork). However, under certain conditions, we may enforce spawn and override the value of VLLM_WORKER_MULTIPROC_METHOD. _maybe_force_spawn() _sync_visible_devices_env_vars() mp_method envs.VLLM_WORKER_MULTIPROC_METHOD return multiprocessing.get_context(mp_method)这个函数是默认 fork 按需强制 spawn策略的统一执行点调用方包括多进程 worker 执行器vllm/v1/executor/multiproc_executor.py 中用它创建 worker 进程context get_mp_context()并基于该 context 创建跨进程锁与消息队列引擎核心进程协调器vllm/v1/engine/coordinator.py引擎工具代码vllm/v1/engine/utils.py创建引擎核心子进程以及 vllm/v1/engine/utils.pyget_mp_context().Queue()创建跨进程队列。值得注意的是vllm/v1/executor/multiproc_executor.py 里还针对两种方法做了不同的 fd 处理使用fork时会跟踪 worker 继承的 socket 文件描述符inherited_fds以便在后续 worker 中关闭它们避免 fd 泄漏——这是默认 fork策略下必须处理的底层细节。CLI 入口默认改用spawn设计文档指出当主进程由vllm命令控制时由于官方控制了入口代码必然有__main__保护会使用兼容性最广泛的spawn。当前仓库中这一行为实现在 vllm/entrypoints/serve/utils/api_utils.pyif VLLM_WORKER_MULTIPROC_METHOD not in os.environ: logger.debug(Setting VLLM_WORKER_MULTIPROC_METHOD to spawn) os.environ[VLLM_WORKER_MULTIPROC_METHOD] spawn源码注释也解释了原因我们只在 CLI 入口这里设置因为改成spawn会破坏一些把 vLLM 当库使用的现有代码。此外forkserver也被 API server 入口显式识别处理见 vllm/entrypoints/launchers/api_server/entry.py。特定平台的强制策略XPU 平台vllm/platforms/xpu.py 中若用户未显式设置则强制VLLM_WORKER_MULTIPROC_METHODspawn对应设计文档中提到的 XPU 执行器强制 spawn 的做法CPU 平台vllm/platforms/cpu.py 中直接写入os.environ[VLLM_WORKER_MULTIPROC_METHOD] spawnNUMA 绑定vllm/utils/numa_utils.py 读取该变量若不为spawn则提示用户设置VLLM_WORKER_MULTIPROC_METHODspawn以启用 NUMA 绑定。引擎核心多进程开关VLLM_ENABLE_V1_MULTIPROCESSING设计文档还记录了 v1 引擎核心多进程的演进早期存在环境变量VLLM_ENABLE_V1_MULTIPROCESSING控制是否在独立进程中运行 v1 引擎核心且当时默认关闭原因正是上文所述的依赖兼容性与库使用场景。启用时v1LLMEngine会创建新进程运行引擎核心。在当前仓库中该变量仍然存在且语义一致但默认值已改为开启定义在 vllm/envs.pyVLLM_ENABLE_V1_MULTIPROCESSING: bool True读取逻辑见 vllm/envs.py默认1vllm/v1/engine/llm_engine.py 在from_vllm_config中把multiprocess_modeenvs.VLLM_ENABLE_V1_MULTIPROCESSING传给引擎构造vllm/v1/engine/llm_engine.py 在from_engine_args中同样据此决定是否为LLMEngine开启多进程模式若使用单进程执行器uniprocvllm/v1/executor/uniproc_executor.py 会断言VLLM_ENABLE_V1_MULTIPROCESSING为假否则报错提示用户设置VLLM_ENABLE_V1_MULTIPROCESSING0vllm/config/parallel.py 在特定并行配置下也会自动将其置为0vllm/benchmarks/startup.py 在收集启动指标时显式禁用多进程以获得干净的进程视图。这一开关对调试非常有用排查多进程相关问题例如子进程中pdb断点失效时可以临时将其设为0让调度器留在主进程中运行方便使用原生调试器官方 docs/usage/troubleshooting.md 的 Breakpoints 一节也给出了同样的建议import os os.environ[VLLM_ENABLE_V1_MULTIPROCESSING] 0故障排查识别强制 spawn警告与__main__保护缺失当以库方式使用 vLLM 且事先初始化了 CUDA这一已知失败场景发生时用户会看到两条相互印证的信息与设计文档给出的示例一致完整内容见 docs/usage/troubleshooting.md 的 Python multiprocessing 一节第一条是 vLLM 的警告日志即 vllm/utils/system_utils.py 中logger.warning的输出形式WARNING ... multiproc_worker_utils.py:281] CUDA was previously initialized. We must use the spawn multiprocessing start method. Setting VLLM_WORKER_MULTIPROC_METHOD to spawn. See https://docs.vllm.ai/en/latest/usage/troubleshooting.html#python-multiprocessing for more information.第二条是 Python 标准库抛出的RuntimeErrorRuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. This probably means that you are not using fork to start your child processes and you have forgotten to use the proper idiom in the main module: if __name__ __main__: freeze_support() ...这两条信息合起来的含义是CUDA 已初始化导致 vLLM 被迫改用spawn而你的主模块没有__main__保护spawn 子进程在引导阶段重入主模块代码失败。修复方式是把 vLLM 的调用挪进__main__保护块内# 错误写法顶层直接创建 LLM import vllm llm vllm.LLM(...)# 正确写法 if __name__ __main__: import vllm llm vllm.LLM(...)另一种思路是既然根因是CUDA 先于 vLLM 初始化也可以在调用 vLLM 之前避免初始化 CUDA例如把无关的torch.cuda操作移出 vLLM 进程启动路径从而让 vLLM 保持默认的fork方式或者显式设置VLLM_WORKER_MULTIPROC_METHODspawn并接受需要__main__保护的事实。备选方案评估为什么 vLLM 选择了尽力而为设计文档专门记录了三个被否决的备选方案理解它们的取舍有助于理解当前实现为何看起来复杂检测调用方是否有__main__保护可以区分当前是原始主进程还是 spawn 出的子进程但如何检测用户代码中是否存在__main__保护并不简单该方案被判定为不切实际而放弃。全面改用forkserver看似优雅但其服务器进程本身以 spawn 方式创建遇到与spawn相同的库场景重入问题没有实质改善。永远强制spawn可以把问题简单化并明确文档要求作为库使用时必须加__main__保护但这会破坏现有用户的代码、降低LLM类开箱即用的体验。vLLM 选择把复杂度留在引擎内部做尽力而为的方法选择而不是把负担推给用户。对应的三条规则正是当前实现所遵循的默认fork当确认自己控制主进程通过vllmCLI 启动时改用spawnvllm/entrypoints/serve/utils/api_utils.py;检测到 CUDA 已初始化时强制spawn并发出指导性警告vllm/utils/system_utils.py。未来方向设计文档最后指出了两条可能的演进路径供后续关注仓库演进时参考实现类forkserver的自管理进程方案由 vLLM 自行启动一个vllm-manager子进程并附带自定义 worker 管理入口绕开 Python 标准库三种启动方式的固有缺陷探索更适合的第三方进程管理库例如 joblib 生态的loky等其进程语义可能对CUDA 库使用这一矛盾组合更友好。小结vLLM 的多进程策略核心是一个带兜底降级的环境变量VLLM_WORKER_MULTIPROC_METHOD默认fork所有 worker/引擎核心进程的创建都经由 get_mp_context() 统一取 context在 CUDA/XPU 已初始化、Ray actor、--numa-bind、WSL 等条件下_maybe_force_spawn() 会自动覆盖为spawn并记录原因这是尽力而为策略的关键一环CLI 入口serve默认改设spawnXPU/CPU 平台直接强制spawn体现了按场景分层选择的思想VLLM_ENABLE_V1_MULTIPROCESSING控制引擎核心是否独立进程运行当前默认为开启设为0是调试子进程问题的有效手段遇到 CUDA was previously initialized 警告加 bootstrap 阶段RuntimeError时标准修复是在if __name__ __main__:保护块内初始化 vLLM详见 docs/usage/troubleshooting.md。【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考