Python 多进程¶
调试¶
请参阅故障排除页面,了解已知问题及其解决方法。
引言¶
警告
源代码引用指向了编写本文时(2024 年 12 月)的代码状态。
在 vLLM 中使用 Python 多进程变得复杂,原因如下:
- 将 vLLM 作为库使用,以及无法控制使用 vLLM 的代码
- 多进程方法与 vLLM 依赖项之间不同程度的不兼容性
本文档描述了 vLLM 如何应对这些挑战。
多进程方法¶
-
spawn
- 启动一个新的 Python 进程。Windows 和 macOS 上的默认方法。 -
fork
- 使用os.fork()
来分叉 Python 解释器。Python 3.14 版本之前在 Linux 上的默认方法。 -
forkserver
- 启动一个服务器进程,该进程将根据请求分叉一个新的进程。Python 3.14 及更新版本在 Linux 上的默认方法。
权衡¶
fork
是最快的方法,但与使用线程的依赖项不兼容。如果您在 macOS 下使用 fork
,可能会导致进程崩溃。
spawn
与依赖项更兼容,但在将 vLLM 用作库时可能会出现问题。如果使用 vLLM 的代码没有使用 __main__
guard(即 if __name__ == "__main__":
),当 vLLM 启动新进程时,该代码可能会被意外地重新执行。这可能导致无限递归等问题。
forkserver
将启动一个新的服务器进程,该进程将根据请求分叉新的进程。不幸的是,当 vLLM 用作库时,这与 spawn
存在相同的问题。服务器进程是作为启动的新进程创建的,它将重新执行未受 __main__
guard 保护的代码。
对于 spawn
和 forkserver
,进程不应依赖于继承任何全局状态,而 fork
则会继承全局状态。
与依赖项的兼容性¶
多个 vLLM 依赖项表明偏好或要求使用 spawn
- https://pytorch.ac.cn/docs/stable/notes/multiprocessing.html#cuda-in-multiprocessing
- https://pytorch.ac.cn/docs/stable/multiprocessing.html#sharing-cuda-tensors
- https://docs.habana.ai/en/latest/PyTorch/Getting_Started_with_PyTorch_and_Gaudi/Getting_Started_with_PyTorch.html?highlight=multiprocessing#torch-multiprocessing-for-dataloaders
更准确地说,是在初始化这些依赖项后使用 fork
存在已知问题。
当前状态 (v0)¶
环境变量 VLLM_WORKER_MULTIPROC_METHOD
可用于控制 vLLM 使用哪种方法。当前的默认方法是 fork
。
当我们知道我们拥有该进程(因为使用了 vllm
命令)时,我们使用 spawn
,因为它兼容性最广。
multiproc_xpu_executor
强制使用 spawn
。
还有其他一些地方硬编码了 spawn
的使用
- https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/distributed/device_communicators/custom_all_reduce_utils.py#L135
- https://github.com/vllm-project/vllm/blob/d05f88679bedd73939251a17c3d785a354b2946c/vllm/entrypoints/openai/api_server.py#L184
相关 PR
v1 中的先前状态¶
有一个环境变量 VLLM_ENABLE_V1_MULTIPROCESSING
用于控制 v1 引擎核心是否使用多进程。该变量默认为关闭。
当它被启用时,v1 LLMEngine
会创建一个新进程来运行引擎核心。
- 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
由于上述所有原因,它默认是关闭的——与依赖项的兼容性以及将 vLLM 用作库的代码。
v1 中所做更改¶
使用 Python 的 multiprocessing
没有一个到处都适用的简单解决方案。第一步,我们可以让 v1 达到一种状态,即它“尽力而为”地选择多进程方法以最大限度地提高兼容性。
- 默认使用
fork
。 - 当我们知道我们控制主进程时(执行了
vllm
),使用spawn
。 - 如果我们检测到
cuda
之前已被初始化,强制使用spawn
并发出警告。我们知道fork
会中断,所以这是我们能做的最好的选择。
在这种情况下已知会中断的情况是,将 vLLM 用作库的代码在调用 vLLM 之前初始化了 cuda
。我们发出的警告应指导用户添加 __main__
guard 或禁用多进程。
如果发生该已知失败情况,用户将看到两条解释正在发生什么的消息。首先,来自 vLLM 的日志消息
WARNING 12-11 14:50:37 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.com.cn/en/latest/usage/debugging.html#python-multiprocessing
for more information.
其次,Python 本身将抛出一个异常并附带详细解释
RuntimeError:
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()
...
The "freeze_support()" line can be omitted if the program
is not going to be frozen to produce an executable.
To fix this issue, refer to the "Safe importing of main module"
section in https://docs.pythonlang.cn/3/library/multiprocessing.html
考虑过的替代方案¶
检测是否存在 __main__
guard¶
有人建议,如果我们能检测出将 vLLM 用作库的代码是否使用了 __main__
guard,我们的表现可能会更好。这篇stackoverflow 帖子来自一位面临同样问题的库作者。
可以检测我们是否处于原始的 __main__
进程中,或后续启动的进程中。但是,直接检测代码中是否存在 __main__
guard 似乎并不容易。
此选项因不切实际而被放弃。
使用 forkserver
¶
起初看起来 forkserver
是一个不错的解决方案。然而,它的工作方式在将 vLLM 用作库时也带来了与 spawn
相同的挑战。
始终强制使用 spawn
¶
一种清理方法是始终强制使用 spawn
,并说明将 vLLM 用作库时需要使用 __main__
guard。这不幸会破坏现有代码并使 vLLM 更难使用,违背了使 LLM
类尽可能易于使用的愿望。
我们不会将此负担强加给用户,而是会保留复杂性,尽最大努力使一切正常工作。
未来工作¶
未来我们可能需要考虑一种不同的工作进程管理方法,以解决这些挑战。
-
我们可以实现类似于
forkserver
的功能,但让进程管理器成为我们最初通过运行自己的子进程和用于工作进程管理的自定义入口点(启动vllm-manager
进程)来启动的东西。 -
我们可以探索其他可能更适合我们需求的库。考虑的例子