跳到内容

架构概述

本文档提供了 vLLM 架构的概览。

入口点

vLLM 提供了多个用于与系统交互的入口点。下图展示了它们之间的关系。

Entrypoints Diagram

LLM 类

LLM 类提供了用于离线推理的主要 Python 接口,即无需使用单独的模型推理服务器即可与模型进行交互。

以下是 LLM 类用法示例

代码
from vllm import LLM, SamplingParams

# Define a list of input prompts
prompts = [
    "Hello, my name is",
    "The capital of France is",
    "The largest ocean is",
]

# Define sampling parameters
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)

# Initialize the LLM engine with the OPT-125M model
llm = LLM(model="facebook/opt-125m")

# Generate outputs for the input prompts
outputs = llm.generate(prompts, sampling_params)

# Print the generated outputs
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")

更多 API 详细信息,请参见 API 文档中的 离线推理 部分。

LLM 类的代码可以在 vllm/entrypoints/llm.py 中找到。

兼容 OpenAI 的 API 服务器

vLLM 的第二个主要接口是通过其兼容 OpenAI 的 API 服务器。该服务器可以使用 vllm serve 命令启动。

vllm serve <model>

vllm CLI 的代码可以在 vllm/entrypoints/cli/main.py 中找到。

有时您可能会看到直接使用 API 服务器入口点,而不是通过 vllm CLI 命令。例如:

python -m vllm.entrypoints.openai.api_server --model <model>

警告

python -m vllm.entrypoints.openai.api_server 已弃用,未来版本中可能不再支持。

该代码可以在 vllm/entrypoints/openai/api_server.py 中找到。

有关 API 服务器的更多详细信息,请参见 兼容 OpenAI 的服务器 文档。

V1 进程架构

vLLM V1 使用多进程架构来分离关注点并最大限度地提高吞吐量。理解此架构对于在您的部署中正确配置 CPU 资源非常重要。关键进程包括:

API 服务器进程

API 服务器进程处理 HTTP 请求(例如,兼容 OpenAI 的 API),执行输入处理(分词、多模态数据加载),并将结果流式传输回客户端。它通过 ZMQ 套接字与引擎核心进程通信。

默认情况下,有 **1 个 API 服务器进程**,但当使用数据并行时,API 服务器的数量会自动扩展以匹配数据并行大小。这也可以通过 --api-server-count 标志手动配置。每个 API 服务器通过 ZMQ 以多对多拓扑连接到**所有**引擎核心,使任何 API 服务器都能将请求路由到任何引擎核心。每个 API 服务器进程使用多个 CPU 线程进行媒体加载(由 VLLM_MEDIA_LOADING_THREAD_COUNT 控制,默认为 8)。

代码可以在 vllm/entrypoints/openai/api_server.py vllm/v1/utils.py 中找到。

引擎核心进程

引擎核心进程运行调度器,管理 KV 缓存,并协调 GPU 工作进程之间的模型执行。它运行一个忙碌循环,不断调度请求并向 GPU 工作进程分发工作。

每个数据并行等级有 **1 个引擎核心进程**。例如,使用 --data-parallel-size 4 时,会有 4 个引擎核心进程。

代码可以在 vllm/v1/engine/core.py vllm/v1/engine/utils.py 中找到。

GPU 工作进程

每个 GPU 由一个专用的工作进程管理。工作进程加载模型权重,执行前向传递,并管理 GPU 内存。工作进程与其所属的引擎核心进程进行通信。

每个 GPU 有 **1 个工作进程**。GPU 工作进程总数等于每个引擎核心的 tensor_parallel_size x pipeline_parallel_size

代码可以在 vllm/v1/executor/multiproc_executor.py vllm/v1/worker/gpu_worker.py 中找到。

DP 协调器进程(条件触发)

当使用数据并行(--data-parallel-size > 1)时,一个额外的协调器进程会管理各 DP 等级之间的负载均衡,并为 MoE 模型协调同步的前向传递。

有 **1 个 DP 协调器进程**(仅在启用了数据并行时)。

代码可以在 vllm/v1/engine/coordinator.py 中找到。

进程数量总结

对于具有 N 个 GPU、TP 张量并行大小、DP 数据并行大小和 A 个 API 服务器数量的部署:

进程类型 数量 注意事项
API 服务器 A (默认 DP) 处理 HTTP 请求和输入处理
引擎核心 DP (默认 1) 调度器和 KV 缓存管理
GPU 工作进程 N (= DP x PP x TP) 每个 GPU 一个,执行模型前向传递
DP 协调器 如果 DP > 1 则为 1,否则为 0 跨 DP 等级的负载均衡
总计 A + DP + N (+ 1 如果 DP > 1)

例如,一个具有 4 个 GPU 的典型单节点部署 (vllm serve -tp=4) 拥有:

  • 1 个 API 服务器 + 1 个引擎核心 + 4 个 GPU 工作进程 = **6 个进程**

V1 Process Architecture - TP=4

一个具有 8 个 GPU 的数据并行部署 (vllm serve -tp=2 -dp=4) 拥有:

  • 4 个 API 服务器 + 4 个引擎核心 + 8 个 GPU 工作进程 + 1 个 DP 协调器 = **17 个进程**

V1 Process Architecture - TP=2, DP=4

有关 CPU 资源调整建议,请参见 GPU 部署的 CPU 资源

LLM 引擎

LLMEngineAsyncLLMEngine 类是 vLLM 系统运行的核心,处理模型推理和异步请求处理。

LLMEngine Diagram

LLMEngine

LLMEngine 类是 vLLM 引擎的核心组件。它负责接收来自客户端的请求并从模型生成输出。LLMEngine 包括输入处理、模型执行(可能分布在多个主机和/或 GPU 上)、调度和输出处理。

  • **输入处理**:使用指定的分词器处理输入文本的分词。
  • **调度**:选择在每一步处理哪些请求。
  • **模型执行**:管理语言模型的执行,包括跨多个 GPU 的分布式执行。
  • **输出处理**:处理模型生成的输出,将语言模型的 token ID 解码为人类可读的文本。

LLMEngine 的代码可以在 vllm/engine/llm_engine.py 中找到。

AsyncLLMEngine

AsyncLLMEngine 类是 LLMEngine 类的异步包装器。它使用 asyncio 创建一个持续处理传入请求的后台循环。AsyncLLMEngine 专为在线服务而设计,可以处理多个并发请求并将输出流式传输给客户端。

兼容 OpenAI 的 API 服务器使用 AsyncLLMEngine。此外还有一个演示 API 服务器,作为一个更简单的示例,位于 vllm/entrypoints/api_server.py

AsyncLLMEngine 的代码可以在 vllm/engine/async_llm_engine.py 中找到。

工作进程 (Worker)

工作进程是运行模型推理的进程。vLLM 遵循使用一个进程控制一个加速器设备(如 GPU)的通用做法。例如,如果我们使用张量并行大小为 2 和流水线并行大小为 2,我们将总共有 4 个工作进程。工作进程由其 ranklocal_rank 标识。rank 用于全局编排,而 local_rank 主要用于分配加速器设备以及访问本地资源(如文件系统和共享内存)。

模型运行器 (Model Runner)

每个工作进程都有一个模型运行器对象,负责加载和运行模型。大部分模型执行逻辑驻留在此,例如准备输入张量和捕获 CUDA 图。

模型 (Model)

每个模型运行器对象都有一个模型对象,即实际的 torch.nn.Module 实例。参见 huggingface_integration 了解各种配置如何影响我们最终得到的类。

类层次结构

下图显示了 vLLM 的类层次结构

Class Hierarchy

在此类层次结构背后有几个重要的设计选择:

1. **可扩展性**:层次结构中的所有类都接受一个包含所有必要信息的配置对象。VllmConfig 类是传递的主要配置对象。类层次结构相当深,每个类都需要读取其感兴趣的配置。通过将所有配置封装在一个对象中,我们可以轻松地传递配置对象并访问所需的配置。假设我们想添加一个新功能(鉴于 LLM 推理领域的发展速度,这种情况经常发生),该功能仅涉及模型运行器。我们将不得不向 VllmConfig 类添加新的配置选项。由于我们传递的是整个配置对象,我们只需将配置选项添加到 VllmConfig 类中,模型运行器即可直接访问它。我们不需要更改引擎、工作进程或模型类的构造函数来传递新配置选项。

2. **统一性**:模型运行器需要一个统一的接口来创建和初始化模型。vLLM 支持 50 多种主流开源模型类型。每种模型都有其特定的初始化逻辑。如果构造函数签名因模型而异,模型运行器将不知道如何在没有复杂且容易出错的检查逻辑的情况下相应地调用构造函数。通过使模型类的构造函数统一,模型运行器可以轻松地创建和初始化模型,而无需了解特定的模型类型。这也适用于组合模型。视觉语言模型通常由视觉模型和语言模型组成。通过统一构造函数,我们可以轻松地创建视觉模型和语言模型,并将它们组合成视觉语言模型。

注意

为了支持此更改,所有 vLLM 模型的签名已更新为:

def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):

为了避免意外传递错误的参数,构造函数现在仅支持关键字参数。这确保如果传递了旧的配置,构造函数将引发错误。vLLM 开发人员已经对 vLLM 内部的所有模型进行了此项更改。对于树外 (out-of-tree) 注册的模型,开发人员需要更新其模型,例如通过添加填充 (shim) 代码来适配旧的构造函数签名以适应新的签名。

代码
class MyOldModel(nn.Module):
    def __init__(
        self,
        config,
        cache_config: Optional[CacheConfig] = None,
        quant_config: Optional[QuantizationConfig] = None,
        lora_config: Optional[LoRAConfig] = None,
        prefix: str = "",
    ) -> None:
        ...

from vllm.config import VllmConfig
class MyNewModel(MyOldModel):
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        config = vllm_config.model_config.hf_config
        cache_config = vllm_config.cache_config
        quant_config = vllm_config.quant_config
        lora_config = vllm_config.lora_config
        super().__init__(config, cache_config, quant_config, lora_config, prefix)

from packaging import version
if version.parse(__version__) >= version.parse("0.6.4"):
    MyModel = MyNewModel
else:
    MyModel = MyOldModel

这样,模型就可以同时与新旧版本的 vLLM 一起工作。

3. **初始化时的分片和量化**:某些功能需要更改模型权重。例如,张量并行需要对模型权重进行分片,量化需要对模型权重进行量化。实现此功能的可能方法有两种。一种是在模型初始化后更改模型权重。另一种是在模型初始化期间更改模型权重。vLLM 选择了后者。第一种方法不适用于大型模型。假设我们想在 16 个 H100 80GB GPU 上运行 405B 模型(大约 810GB 权重)。理想情况下,每个 GPU 只应加载 50GB 权重。如果我们在模型初始化后更改模型权重,我们需要将完整的 810GB 权重加载到每个 GPU,然后分片权重,这将导致巨大的内存开销。相反,如果我们在模型初始化期间分片权重,每一层只会创建其所需权重的分片,从而导致小得多的内存开销。同样的逻辑也适用于量化。请注意,我们还向模型的构造函数添加了一个额外的参数 prefix,以便模型可以根据前缀以不同的方式进行自我初始化。这对于非均匀量化很有用,其中模型的不同部分被不同地量化。prefix 对于顶级模型通常是一个空字符串,而对于子模型则是一个像 "vision""language" 这样的字符串。通常,它与检查点文件中模块的状态字典名称相匹配。

这种设计的一个缺点是很难为 vLLM 中的各个组件编写单元测试,因为每个组件都需要由完整的配置对象初始化。我们通过提供默认初始化函数来解决这个问题,该函数创建一个所有字段均设置为 None 的默认配置对象。如果我们要测试的组件只关心配置对象中的几个字段,我们可以创建一个默认配置对象并设置我们关心的字段。这样,我们就可以独立测试该组件。请注意,vLLM 中的许多测试都是测试整个系统的端到端测试,因此这不是一个大问题。

总之,完整的配置对象 VllmConfig 可以被视为在所有 vLLM 类之间共享的引擎级全局状态。