Python 多进程#

调试#

请参阅故障排除页面,了解已知问题以及如何解决这些问题。

简介#

重要提示

源代码参考的是 2024 年 12 月编写时的代码状态。

在 vLLM 中使用 Python 多进程因以下原因而变得复杂:

  • 将 vLLM 用作库以及无法控制使用 vLLM 的代码

  • 多进程方法和 vLLM 依赖项之间存在不同程度的不兼容性

本文档描述了 vLLM 如何应对这些挑战。

多进程方法#

Python 多进程方法包括:

  • spawn - 衍生一个新的 Python 进程。这将是 Python 3.14 及更高版本的默认设置。在 macOS 中,这已经是默认设置。

  • fork - 使用 os.fork() 来 fork Python 解释器。这是 Python 3.14 之前版本的默认设置。

  • forkserver - 衍生一个服务器进程,该进程将在请求时 fork 一个新进程。

权衡#

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

spawn 与依赖项更兼容,但当 vLLM 用作库时可能会出现问题。如果消费代码未使用 __main__ 保护(if __name__ == "__main__":),则当 vLLM 衍生新进程时,代码将被意外地重新执行。这可能导致无限递归以及其他问题。

forkserver 将衍生一个新的服务器进程,该进程将按需 fork 新进程。不幸的是,当 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 用作库的代码,该代码在调用 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/getting_started/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__ 保护#

有人建议,如果我们能够检测到将 vLLM 用作库的代码是否已设置 __main__ 保护,我们可以表现得更好。stackoverflow 上的这篇文章来自一位面临相同问题的库作者。

可以检测我们是否在原始的 __main__ 进程中,还是在后续衍生的进程中。但是,检测代码中是否存在 __main__ 保护似乎并非易事。

此选项已被认为不切实际而放弃。

使用 forkserver#

乍一看,forkserver 似乎是解决问题的好方法。但是,它的工作方式与 vLLM 用作库时 spawn 所带来的挑战相同。

始终强制 spawn#

清理此问题的一种方法是始终强制使用 spawn,并记录当将 vLLM 用作库时,需要使用 __main__ 保护。不幸的是,这将破坏现有代码,并使 vLLM 更难使用,这违反了使 LLM 类尽可能易于使用的愿望。

与其将此强加给我们的用户,我们将保留复杂性,尽最大努力使事情正常运行。

未来工作#

我们可能希望在未来考虑一种不同的工作进程管理方法,以解决这些挑战。

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

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