vLLM系统拆解-01-架构设计:为什么推理引擎不能是一个进程

model.generate() 很好用,但它解决的是单请求问题——一个输入进去,一个输出出来,跑完拉倒。

真实的 LLM serving 场景不是这样:并发请求是常态,每个请求 prompt 长度不同、生成进度不一,有的刚开始 Prefill,有的已经 Decode 到一半,还有的在等 KV Cache 空间释放。在这种状态下,GPU 利用效率、请求排队延迟和整体吞吐,都不是"把模型跑起来"能解决的问题。

vLLM 的定位是高吞吐、显存高效的大模型推理与服务引擎。它不靠某一个 attention kernel 快多少取胜,而是在并发请求的上下文里提供一套完整的调度机制、KV Cache 管理和分层执行架构。

这篇文章从 vLLM 的整体架构出发,解释它为什么采用多进程设计,每个进程负责什么,以及一次请求是怎样在这套架构里流转的。理解这张"总地图",是读懂后续 scheduler、KV Cache、PagedAttention 等机制的前提。


LLM serving 到底难在哪

单请求场景关注的是:模型能不能装进显存、forward 够不够快、结果是否正确。

服务型推理面对的是另一组问题:

  • 大量请求动态到达,prompt 长度、生成长度各不相同
  • 有的请求处于 Prefill 阶段(计算初始 KV Cache),有的处于 Decode 阶段(逐 token 生成)
  • KV Cache 随 Decode 持续增长,且不同请求的 KV Cache 之间无法直接共享显存空间
  • 需要流式返回,不能等全部生成完再输出
  • 既要控制首 token 延迟(Time To First Token,TTFT),也要维持较高的整体吞吐

这些特征加在一起,让问题的核心变成:如何把大量处于不同状态的请求,持续、高效地编排进 GPU 的执行流程

有一个角度可以帮助理解 vLLM 在做什么:把它当成"大模型推理操作系统"。和操作系统类似,它接收外部任务(请求)、管理资源(GPU 显存、KV Cache)、做调度(决定谁先运行、谁暂停)、做隔离(不同请求彼此独立)、做复用(prefix caching)、做回收(请求结束后释放 KV Cache),还要支持并行扩展(多 GPU、多副本)。这不是官方定义,但作为理解模型它很准确。


两种使用入口

vLLM 同时支持两类场景,对应两种入口:

入口一:Python 离线调用

1
2
3
4
5
6
from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct")
sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=128)
outputs = llm.generate(["什么是 KV Cache?"], sampling_params)
print(outputs[0].outputs[0].text)

这种模式更像库调用,适合离线推理、模型测试、写 benchmark、嵌入到自有程序里。对应的核心类是 LLMLLMEngine

入口二:OpenAI-compatible HTTP server

1
vllm serve Qwen/Qwen2.5-1.5B-Instruct

启动后,外部服务通过 OpenAI 兼容接口(/v1/chat/completions/v1/completions 等)访问。适合工程/平台团队把 vLLM 部署成一个模型服务,对接已有上层应用。

两种入口存在的原因:研究/开发者需要在 Python 里直接用,工程/平台团队需要一个 HTTP 服务。vLLM 的做法是把核心推理引擎独立出来,再在上面承接不同入口——这个思路贯穿整个架构设计。


为什么必须是多进程

理论上用一个进程做完所有事(接 HTTP、tokenization、调度、调 GPU、流式返回)是可以的。但这条路很快遇到工程天花板。

CPU 任务与 GPU 任务相互干扰

API server 处理 HTTP 请求、序列化/反序列化、tokenization,是典型的 CPU-heavy 工作。调度逻辑是高频小循环,对延迟极其敏感——每一轮 Decode 都要重新决定哪些请求继续执行、KV Cache 够不够用。两类工作放在同一进程,网络请求的抖动会直接拖慢调度频率,进而拉低 GPU 利用率、增加 token 间延迟(Inter-Token Latency,ITL)。

服务接口和核心引擎不该耦合

vLLM 同时支持 Python 离线调用和 HTTP server 两种入口。如果 HTTP 层和调度逻辑写死在一起,换协议、加新接口、单独优化 API server 都会很麻烦。核心引擎需要是一个独立能力层,不同入口挂在上面。

多 GPU 场景天然要求多进程

每张 GPU 有独立的显存、CUDA context 和执行队列。Tensor Parallel、Pipeline Parallel 等并行策略,最终都要落到不同 GPU 上执行。用一个进程管理多张 GPU 的运行状态,会引入不必要的上下文切换复杂度、错误隔离问题和调试难度。

vLLM V1 的解法:把服务入口、调度核心、GPU 执行三层彻底拆开,每层只做自己最擅长的事。官方明确将这次重构的目标定为:代码简洁模块化、CPU 额外开销近乎为零、关键优化统一到一套架构里,以及尽量零配置默认启用合理优化。


整体架构:四个进程角色

vLLM V1 的进程架构最多包含四个角色(DP Coordinator 仅在 data parallel 场景出现):

flowchart TD
    Client["用户 / 应用\nHTTP 请求 / Python 调用"]

    subgraph API_Proc["API Server Process"]
        direction TB
        APIServer["API Server\n- 解析请求 / chat template\n- Tokenization\n- 多模态输入处理\n- 流式返回结果"]
    end

    subgraph Core_Proc["Engine Core Process"]
        direction TB
        Scheduler["Scheduler\n调度决策 / token budget"]
        KVMgr["KV Cache Manager\nblock 分配 / 回收"]
    end

    subgraph Worker_Procs["GPU Worker Process(es)"]
        direction LR
        W0["GPU Worker 0\nModel Runner\nForward / KV Cache 写入"]
        W1["GPU Worker 1\nModel Runner\nForward / KV Cache 写入"]
        Wn["..."]
    end

    subgraph DP_Proc["DP Coordinator Process\n仅 data parallel 场景"]
        DPCoord["DP Coordinator\n负载均衡 / MoE 同步协调"]
    end

    Client -- 请求进入 --> API_Proc
    API_Proc -- 内部 request 对象 --> Core_Proc
    Core_Proc -- 下发可执行 batch --> Worker_Procs
    Worker_Procs -- 输出 token / 状态 --> Core_Proc
    Core_Proc -- 结果 --> API_Proc
    API_Proc -- 流式返回 --> Client
    DP_Proc -. 跨副本协调 .-> Core_Proc

四个角色的职责和源码位置:

进程角色 核心职责 关键源码路径
API Server 接收 HTTP 请求、tokenization、多模态输入处理、结果流式返回 vllm/entrypoints/openai/api_server.py
Engine Core 运行 scheduler、管理 KV Cache、协调 GPU workers、busy loop 持续调度 vllm/v1/engine/core.py
GPU Worker 加载模型权重、管理本 GPU 显存、执行 forward、写入更新 KV Cache vllm/v1/worker/gpu_worker.py
DP Coordinator DP ranks 之间负载均衡、MoE 模型同步 forward 协调(条件性存在) vllm/v1/engine/coordinator.py

API Server:前台,不是大脑

API Server 的工作是"把用户能理解的输入,转成推理引擎能执行的内部表示,并把结果原路送回"。具体包括:解析 HTTP 请求和参数、应用 chat template、tokenization、多模态数据加载(如果有),然后把生成好的内部 request 对象发给 Engine Core,最后把生成结果流式回传给客户端。

把 API Server 单独拆出来有三个直接收益:

  • 服务层和核心引擎解耦:HTTP 层如何变(换协议、加新接口),不影响调度和执行逻辑。
  • 横向扩展更自然:data parallel 开启时,API Server 可以随副本数一起扩,与 Engine Core 的扩展逻辑解耦。
  • CPU 资源规划更清晰:tokenization 和请求处理是明显的 CPU 密集操作,单独进程便于规划线程数和核数,不和调度侧竞争 CPU 时间。

Engine Core:系统的调度中枢

Engine Core 是整个系统的控制中枢,官方的描述是:运行 scheduler、管理 KV Cache、协调 GPU workers,以 busy loop 方式不断循环执行。

"busy loop"是关键词。Engine Core 不是被动等待请求触发,而是持续运行一个调度循环,每一轮都做:

  1. 决定哪些请求进入本轮 batch
  2. 为每个请求分配 token budget
  3. 查询和分配 KV Cache block
  4. 把请求组成可执行 batch
  5. 将 batch 下发给 GPU workers

为了完成这些决策,Engine Core 要持续维护大量动态状态,包括每个请求的当前阶段、已生成 token 数、prompt 长度、是否已完成 Prefill、当前占用的 KV Cache block、batch budget 剩余情况。

Engine Core 为什么必须独立成进程,有三个原因:

  • 调度是高频操作:每一步 Decode 都要重新决策(哪些请求继续、是否需要抢占/回收/推迟),频率极高。
  • 状态维护量大:请求数量越多,状态管理越复杂,与其他进程共享状态会引入不必要的同步开销。
  • 对 CPU 延迟敏感:调度层 CPU 一旦被其他工作拖慢,GPU 就会空转,ITL 直接上升。

GPU Worker:执行者,不是决策者

每张 GPU 对应一个独立的 GPU Worker 进程。职责直接:加载模型权重、管理本 GPU 显存、接受 Engine Core 下发的 batch、执行 forward、把新生成的 KV Cache 写入显存、产生 logits。

“Engine Core 决定做什么,GPU Worker 负责把它做出来”——这是两者的分工边界,Worker 不参与全局请求决策。

一张 GPU 对应一个 worker 是最自然的设计:

  • 每张 GPU 有独立的显存、CUDA context、执行队列,一对一对应职责最清晰
  • 独立进程带来更好的错误隔离——某个 worker 出问题不直接影响其他 worker
  • Tensor Parallel 和 Pipeline Parallel 都需要把计算映射到不同 GPU,"一 GPU 一 worker"天然适配这类映射

DP Coordinator:多副本场景的协调层

DP Coordinator 只在启用 data parallel 时出现。

data parallel 是把一整套 Engine Core + GPU Workers 复制多份,同时服务更多请求。但"多复制几份"不是终点——还需要解决:请求路由到哪个 DP rank、各 rank 负载是否均衡、MoE 模型的专家路由是否需要跨 rank 同步(对 MoE 模型,各 rank 之间需要协调 forward,否则结果可能不一致)。

DP Coordinator 处理的就是这一层的问题:比 Engine Core 更高层的跨副本编排。


LLMEngine / AsyncLLMEngine:逻辑组件 vs 进程角色

这里有一个容易混淆的地方。前面说的是进程角色(API Server Process、Engine Core Process 等),而 LLMEngineAsyncLLMEngine 是代码层面的逻辑组件,不是进程,两个维度不在同一层次。

graph LR
    subgraph logical["逻辑组件(代码层)"]
        LLM["LLM\n用户调用接口"]
        LLMEng["LLMEngine\n同步引擎抽象"]
        AsyncEng["AsyncLLMEngine\n异步引擎抽象"]
        Sched["Scheduler"]
        KVM["KV Cache Manager"]
    end

    subgraph proc["进程角色(运行时)"]
        AProc["API Server Process"]
        CProc["Engine Core Process"]
        WProc["GPU Worker Process"]
    end

    LLM -->|依赖| LLMEng
    AsyncEng -->|被| AProc
    LLMEng -.->|概念上对应| CProc
    Sched -->|运行在| CProc
    KVM -->|运行在| CProc
  • LLM:面向用户的顶层接口,LLM.generate() 就是从这里进入
  • LLMEngine:核心引擎抽象,负责输入处理、调度、模型执行组织、输出处理,适合 Python 离线场景
  • AsyncLLMEngine:异步版本,适合服务端并发请求、流式输出、异步 request lifecycle 管理;OpenAI-compatible API server 使用的是这个,因为服务端不能因为一个请求阻塞其他请求

Engine Core(进程)和 LLMEngine(类)有对应关系,但不是同义词。前者是运行时的进程角色,后者是代码层面的抽象类。


一次请求的完整流转

把前面的模块串起来,看一次请求的完整路径:

sequenceDiagram
    participant C as 用户/客户端
    participant A as API Server
    participant E as Engine Core
    participant W as GPU Worker
    participant S as Sampler

    C->>A: 发起请求(HTTP POST 或 LLM.generate)
    A->>A: 解析参数 / chat template / tokenization / 多模态处理
    A->>E: 提交内部 request 对象

    loop 调度循环(每轮 Decode)
        E->>E: 调度决策:选本轮进入 batch 的请求
        E->>E: 分配 token budget / 分配 KV Cache block
        E->>W: 下发可执行 batch
        W->>W: 准备输入张量 / 调用 Model Runner
        W->>W: 执行 forward / 写入更新 KV Cache
        W->>S: 传递 logits
        S->>S: 采样(greedy / temperature / top-p / top-k 等)
        S->>E: 返回本轮输出 token
        E->>E: 更新请求状态
        E->>A: 传递输出 token
        A->>C: 流式返回 token(如启用)
    end

    E->>E: 检测到结束条件(EOS / max_tokens)
    E->>E: 释放 KV Cache block / 清理请求状态
    A->>C: 返回完整结果

每一步的要点:

第 1 步:输入处理(API Server)
把用户输入转为推理引擎的内部表示——解析请求参数、应用 chat template(对话模式下)、tokenization、多模态数据加载,生成内部 request 对象。

第 2 步:进入引擎(Engine Core)
请求进入 LLMEngine / AsyncLLMEngine 管理的流程,系统开始追踪它的状态:是新请求还是正在 Decode、当前需要多少 KV Cache、batch 能不能塞下它。

第 3 步:调度决策(Engine Core,持续循环)
在 busy loop 里,每轮决定:哪些请求进入本轮 batch、分配多少 token budget、KV Cache block 怎么分配,然后把可执行 batch 下发给 GPU workers。

第 4 步:GPU 执行(GPU Worker)
准备输入张量,调用 Model Runner,执行模型 forward,把新生成的 KV Cache 写入显存,产生 logits。

第 5 步:采样(Sampler)
从 logits 中根据采样策略选出下一个 token。支持 greedy、temperature、top-p、top-k 等策略,以及部分场景下的 beam search。

第 6 步:输出与状态更新
把 token 转为文本、流式回传给客户端(如果启用)、更新请求状态、检查是否满足结束条件。若请求结束,释放其占用的 KV Cache block 和请求状态;若未结束,继续参与下一轮调度。

最后一步有个细节值得注意:一个请求不是"进来执行一次就结束",而是在多个调度轮次中逐步完成 Decode。这就是为什么 vLLM 需要请求生命周期管理,而不是一次函数调用完事。


为什么要持续调度,而不是静态 batch

"静态 batch"的思路是:凑满一批请求,统一跑完,再接下一批。在离线 benchmark 里没问题,在在线服务场景里会遇到几个硬伤:

问题 静态 batch 持续调度(continuous batching)
新请求何时能进入 必须等当前 batch 全部完成 任意 Decode 轮次都可以插入
batch 内请求长度不一 需要填充对齐,短请求浪费算力 按实际状态动态分配 token budget
某个请求提前结束 对应槽位空转到整个 batch 完成 立即释放资源,下一轮可接新请求
GPU 利用率 取决于 batch 内请求状态是否整齐 通过动态拼 batch 维持较高利用率
KV Cache 释放粒度 batch 结束后统一释放 每轮可做细粒度分配和回收

持续调度的核心思想:系统持续运行调度循环,每一轮根据所有活跃请求的当前状态,重新决定这轮 GPU 应该执行什么。这让 vLLM 能够接纳新请求、继续旧请求、动态控制 batch 内容、更好利用 GPU 空隙时间、更精细地管理 KV Cache。

这也是为什么 vLLM 的价值主要体现在 serving 场景,而不是单条离线推理。


V1 重构:为什么需要系统性改造

vLLM V1 不是小修小补,而是对核心架构的一次系统性重构。

V0 阶段,vLLM 快速扩展了对大量模型和硬件的支持,但不同特性是相对独立演进的——prefix caching、chunked prefill、speculative decoding、多模态等功能各自叠加,导致系统复杂度积累,技术债增加,核心调度路径越来越难以优化。

V1 想解决这些问题,目标明确:

  1. 代码更简单、模块化、易于 hack 和扩展:不同特性的实现要能统一在同一套架构里
  2. 近乎零 CPU 额外开销:scheduler 和 sampler 的 CPU 开销在 V1 里被显著压缩
  3. 关键优化统一到一套架构:PagedAttention、prefix caching、chunked prefill 等机制协同工作,而不是各自独立维护
  4. 尽量零配置:默认启用合理优化,减少用户手动调参的负担

这意味着本系列后面讲到的 scheduler、KV Cache 管理、PagedAttention,都是 V1 统一架构下的组成部分,不是独立的优化 trick。


源码路径速查

模块 源码路径
Python 离线入口(CLI) vllm/entrypoints/cli/main.py
OpenAI-compatible HTTP 服务 vllm/entrypoints/openai/api_server.py
同步引擎抽象 vllm/engine/llm_engine.py
异步引擎抽象 vllm/engine/async_llm_engine.py
V1 Engine Core(调度中枢) vllm/v1/engine/core.py
V1 Engine 工具函数 vllm/v1/engine/utils.py
多进程执行器 vllm/v1/executor/multiproc_executor.py
GPU Worker vllm/v1/worker/gpu_worker.py
DP Coordinator vllm/v1/engine/coordinator.py

几个常见混淆点

LLM / LLMEngine / AsyncLLMEngine 的区别

三者是不同层次的抽象:LLM 是面向用户的顶层接口,底层依赖 LLMEngineAsyncLLMEngine 是服务端场景的异步版本,OpenAI API server 使用它,不会因单个请求阻塞其他请求。

Engine Core 和 LLMEngine 不是同一个概念

LLMEngine 是代码层面的类抽象,Engine Core 是 V1 进程架构里负责调度协调的进程角色。两者有对应关系,但不是同义词——一个是逻辑组件,另一个是运行时进程。

API Server 不做推理

它做的是请求接入、输入处理(tokenization 等)和结果回传。调度逻辑在 Engine Core,GPU 计算在 Worker。

GPU Worker 不是调度器

Worker 只负责执行 Engine Core 下发的任务,不参与全局请求决策。决策权在 Engine Core,Worker 专注执行。


这套架构的设计逻辑

把前面的内容串成一条逻辑链:

  1. LLM serving 是动态多请求问题,不是单次 forward 问题
  2. 动态多请求要求有请求生命周期管理
  3. 生命周期管理需要专门的调度中枢(Engine Core),持续运行调度循环
  4. 调度中枢是高频 CPU 循环,必须和 HTTP 服务层解耦,否则互相干扰
  5. GPU 执行天然适合封装成独立 worker,与 GPU 资源一一对应
  6. 多副本 data parallel 场景需要更上层的跨副本协调(DP Coordinator)
  7. 最终形成:API Server → Engine Core → GPU Workers 的分层多进程架构

这条逻辑链,是后续理解 scheduler、KV Cache 管理、PagedAttention、prefix caching 的基础——这些机制运行在这套架构的特定层次里,脱开架构单独看,很容易把它们当成相互独立的优化 trick,而看不到它们之间的系统联系。


系列导航