跳到内容

Python 多进程

调试

请参阅故障排除页面,了解有关已知问题及其解决方法的信息。

简介

重要

源代码引用的是 2024 年 12 月撰写本文时的代码状态。

在 vLLM 中使用 Python 多进程的复杂性在于:

  • 将 vLLM 作为库使用,这限制了对其内部代码的控制;
  • 某些多进程方法与 vLLM 依赖项之间存在不兼容性。

本文档介绍了 vLLM 如何处理这些挑战。

多进程方法

Python 多进程方法包括:

  • spawn - 生成一个新的 Python 进程。这是 Windows 和 macOS 上的默认设置。
  • fork - 使用 os.fork() 来派生 Python 解释器。这是 3.14 版本之前 Linux 系统上 Python 的默认设置。
  • forkserver - 生成一个服务器进程,按需派生新进程。这是 Python 3.14 及更高版本在 Linux 上的默认设置。

权衡

fork 是最快的方法,但与使用线程的依赖项不兼容。在 macOS 上,使用 fork 可能会导致进程崩溃。

spawn 与依赖项的兼容性更好,但当 vLLM 作为库使用时可能会出现问题。如果使用代码没有使用 __main__ 防护 (if __name__ == "__main__":),当 vLLM 生成新进程时,代码会被无意中重复执行。这可能导致无限递归以及其他问题。

forkserver 将生成一个新的服务器进程,按需派生新进程。不幸的是,当 vLLM 作为库使用时,它存在与 spawn 相同的问题。服务器进程作为生成的子进程创建,它会重新执行未被 __main__ 防护保护的代码。

对于 spawnforkserver,进程都不能依赖继承任何全局状态(这是 fork 的情况)。

与依赖项的兼容性

多个 vLLM 依赖项表明它们更倾向于或要求使用 spawn

在初始化这些依赖项之后使用 fork 会存在已知问题。

当前状态 (v0)

环境变量 VLLM_WORKER_MULTIPROC_METHOD 可用于控制 vLLM 使用的方法。目前的默认值是 fork

如果主进程是通过 vllm 命令控制的,则使用 spawn,因为它的兼容性最广。

multiproc_xpu_executor 强制使用 spawn

其他一些零散的地方也硬编码使用了 spawn

相关 PR

v1 中的先前状态

曾经有一个环境变量用于控制 v1 引擎核心中是否使用多进程,即 VLLM_ENABLE_V1_MULTIPROCESSING。该项默认为关闭。

启用该项时,v1 的 LLMEngine 将创建一个新进程来运行引擎核心。

由于上述所有原因(与依赖项的兼容性,以及将 vLLM 作为库使用的代码),它默认是关闭的。

v1 中的变更

Python 的 multiprocessing 没有一种能完美解决所有问题的简单方案。作为第一步,我们可以让 v1 进入一种“尽力而为”的状态,以选择最大限度提高兼容性的多进程方法。

  • 默认使用 fork
  • 当我们知道自己控制主进程时(即执行了 vllm),使用 spawn
  • 如果我们检测到之前已经初始化了 cuda,则强制使用 spawn 并发出警告。我们知道 fork 会导致失败,所以这是我们能做的最好的处理。

在这种情况下,仍然会失败的案例是:在调用 vLLM 之前初始化了 cuda 的库调用代码。我们发出的警告应该指导用户要么添加 __main__ 防护,要么禁用多进程。

如果发生了已知的故障情况,用户将看到两条解释原因的消息。首先,来自 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/troubleshooting.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__ 防护

有人建议,如果我们能检测到将 vLLM 作为库使用的代码是否具有 __main__ 防护,表现会更好。这篇 Stack Overflow 帖子 来自一位面临同样问题的库开发者。

检测我们当前是在原始的 __main__ 进程中,还是在后续生成的进程中是可行的。然而,检测代码中是否存在 __main__ 防护似乎并不直接。

此选项因不可行而被放弃。

使用 forkserver

起初,forkserver 看起来是解决该问题的一个不错方案。然而,它的工作方式与将 vLLM 作为库使用时使用 spawn 面临的挑战相同。

始终强制 spawn

清理此问题的一种方法是始终强制使用 spawn,并记录在使用 vLLM 作为库时必须使用 __main__ 防护。不幸的是,这会破坏现有代码并使 vLLM 更难使用,违反了让 LLM 类尽可能易于使用的初衷。

我们不会将这种负担强加给用户,而是将保持现有的复杂性,尽最大努力让事情正常工作。

未来工作

未来我们可能会考虑一种不同的 worker 管理方法,以规避这些挑战。

  1. 我们可以实现某种类似 forkserver 的功能,但通过运行我们自己的子进程和自定义的 worker 管理入口点(启动一个 vllm-manager 进程)来作为最初的进程管理器。

  2. 我们可以探索更适合我们需求的其他库。需要考虑的示例: