Python 多进程#

调试#

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

简介#

重要提示

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

在 vLLM 中使用 Python 多进程受以下因素影响而变得复杂:

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

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

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

多进程方法#

Python 多进程方法包括

  • spawn - 派生一个新的 Python 进程。这将是 Python 3.14 及更高版本的默认设置。

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

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

权衡#

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 作为库的代码,该代码在调用 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 似乎是解决问题的一个不错的方案。但是,它的工作方式与 spawn 在 vLLM 用作库时提出的挑战相同。

始终强制 spawn#

清理此问题的一种方法是始终强制使用 spawn,并在文档中说明当将 vLLM 用作库时,需要使用 __main__ 守卫。不幸的是,这将破坏现有代码,并使 vLLM 更难使用,从而违反了使 LLM 类尽可能易于使用的愿望。

为了不将此强加给我们的用户,我们将保留复杂性,尽力使事情正常运行。

未来工作#

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

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

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