vLLM内核探秘-第1章 架构总览

《vLLM 内核探秘》完整目录

第1章 架构总览

"The best way to understand a city's traffic system is not to study the route map, but to ride a bus from terminal to terminal."

:::tip 本章要点

  • 跟踪一个推理请求从 HTTP 到 Token 输出的完整旅程
  • 理解 V1 引擎的多进程架构及其设计动因
  • 掌握 vLLM 五大核心子系统的职责边界
  • 认识 V0 到 V1 的架构跃迁:哪些被保留,哪些被重写,为什么
  • 建立全书后续章节的认知地图 :::

1.1 一个请求的完整旅程

让我们从最具体的场景开始。

一个用户向你的 LLM 服务发送了一条消息:"请用一句话解释什么是 PagedAttention。" 这条消息经过网络,抵达你部署的 vLLM 服务。从这一刻起,到用户收到完整回复的那一刻,中间发生了什么?

这趟旅程可以分为六个阶段:

graph LR A["HTTP 请求到达"] --> B["API Server
解析 · 分词"] B --> C["EngineCore
请求入队"] C --> D["Scheduler
调度决策"] D --> E["Worker
GPU 前向计算"] E --> F["API Server
去分词 · 流式输出"] style A fill:#3b82f6,color:#fff,stroke:none style B fill:#8b5cf6,color:#fff,stroke:none style C fill:#ec4899,color:#fff,stroke:none style D fill:#f59e0b,color:#fff,stroke:none style E fill:#10b981,color:#fff,stroke:none style F fill:#6366f1,color:#fff,stroke:none

阶段一:API Server 接收请求

请求首先抵达 API Server 进程(vllm/entrypoints/openai/api_server.py)。这是一个基于 FastAPI 的 HTTP 服务,提供 OpenAI 兼容的接口:/v1/chat/completions/v1/completions 等。

API Server 做三件事:

  1. 参数校验------检查模型名、温度、top_p 等参数是否合法
  2. 分词(Tokenization)------将用户的文本转换为 Token ID 序列。这一步在 API Server 进程中完成,而不是在 GPU Worker 中------这是 V1 架构的一个关键决策,我们稍后会解释为什么
  3. 提交请求------通过 ZMQ Socket 将请求发送给 EngineCore 进程

注意这里的进程分离:API Server 和 EngineCore 运行在不同的进程中,通过 ZMQ IPC(进程间通信)连接。这与 V0 架构有本质区别------V0 中所有逻辑都在同一个进程内。

阶段二:EngineCore 接收并排队

EngineCore(vllm/v1/engine/core.py)是 vLLM V1 的心脏。它运行在一个独立的进程中,有自己的事件循环,不受 HTTP 请求处理的干扰。

收到请求后,EngineCore 将其封装为内部的 Request 对象,加入等待队列。此时请求还没有分配任何 GPU 资源------它在排队等候调度器的宠幸。

阶段三:调度器做出决策

这是最精妙的环节。

每一步推理开始前,调度器vllm/v1/core/sched/scheduler.py)都要回答一个问题:这一步该让哪些请求往前走?每个请求走多少个 Token?

调度器的输出极其简洁------一个字典:

python 复制代码
# {request_id: num_tokens_to_process}
scheduled = {
    "req-001": 128,   # 新请求,预填充 128 个 Token
    "req-002": 1,     # 老请求,解码 1 个 Token
    "req-003": 1,     # 老请求,解码 1 个 Token
    "req-004": 64,    # 新请求的部分预填充(分块)
}

这个看似简单的字典背后,是调度器在显存预算、吞吐量和延迟之间做的精密权衡。它需要考虑:当前有多少空闲的 KV Cache 块?新请求的预填充会不会挤掉正在解码的老请求?分块预填充应该切多大?

我们会在第 3 章详细拆解这个决策过程。

阶段四:KV Cache 分配

调度器决策完成后,KV Cache 管理器vllm/v1/core/kv_cache_manager.py)登场。它需要为调度器选中的请求分配物理显存块。

这里就是 PagedAttention 发挥作用的地方。KV Cache 管理器维护一个 BlockPool------一个由固定大小显存块组成的资源池。每个块能存储 16 个 Token 的 Key-Value 对。分配时不需要连续空间,就像操作系统的内存分页一样,通过一张"块表"(类似页表)记录逻辑块到物理块的映射。

graph TB subgraph "逻辑视图(请求看到的)" L1["Token 0-15"] --> L2["Token 16-31"] --> L3["Token 32-47"] end subgraph "物理视图(GPU 显存)" P7["Block 7"] P2["Block 2"] P15["Block 15"] Pfree["Block 4
(空闲)"] end L1 -.-> P7 L2 -.-> P2 L3 -.-> P15 style L1 fill:#3b82f6,color:#fff,stroke:none style L2 fill:#3b82f6,color:#fff,stroke:none style L3 fill:#3b82f6,color:#fff,stroke:none style P7 fill:#10b981,color:#fff,stroke:none style P2 fill:#10b981,color:#fff,stroke:none style P15 fill:#10b981,color:#fff,stroke:none style Pfree fill:#94a3b8,color:#fff,stroke:none

这种设计消除了显存碎片------V0 时代,一个 2048 Token 的请求需要一整块连续显存,即使只生成了 100 个 Token 也要占着 2048 的坑位。PagedAttention 将浪费率从 60-80% 降到了 4% 以下。

阶段五:GPU 执行前向计算

一切准备就绪后,Executor 将调度结果发送给 Workervllm/v1/worker/gpu_worker.py)。Worker 是真正触碰 GPU 的角色------每张 GPU 卡对应一个 Worker 进程。

Worker 收到的不是完整的请求状态,而是一个差量更新(diff)。它本地缓存了所有活跃请求的状态,每一步只接收"哪些请求新增了、哪些变化了、哪些结束了"。这是 V1 的另一个关键优化------V0 每一步都要广播完整状态,通信开销随并发请求数线性增长。

Worker 调用 ModelRunner 执行模型的前向传播:

  1. 准备输入------从缓存的状态中组装 Token ID、位置编码、注意力掩码
  2. 执行注意力计算------调用 FlashAttention3 内核,利用块表访问分页的 KV Cache
  3. 计算 Logits------模型的最后一层输出每个词表位置的得分
  4. 采样------根据温度、top_p、top_k 等参数从 Logits 中采出下一个 Token

一个 Token 就这样诞生了。

阶段六:去分词与流式输出

新生成的 Token ID 被送回 API Server 进程。API Server 负责去分词------将 Token ID 转换回人类可读的文本片段。

如果用户请求了流式输出(stream=true),每个 Token 会立即通过 Server-Sent Events(SSE)推送给客户端。用户在浏览器中看到文字逐字蹦出来,就是这个过程的体现。

去分词之所以放在 API Server 而非 Worker 中,是 V1 架构的刻意选择。去分词是 CPU 密集型操作,放在 Worker 进程会与 GPU 计算争夺宝贵的执行时间。进程分离让 CPU 工作和 GPU 工作真正并行。

然后,旅程循环往复------EngineCore 启动下一步调度,Worker 生成下一个 Token,直到遇到终止条件(最大长度、EOS Token、停止词)。一个完整的回复就这样一个 Token 一个 Token 地拼出来了。

1.2 V1 多进程架构

上一节的旅程中,你可能已经注意到一个反复出现的关键词:进程分离。这是 V1 架构最重要的设计决策,值得我们停下来仔细审视。

为什么要多进程

V0 的架构是单进程的。LLMEngine 对象在一个 Python 进程中完成所有工作------接收 HTTP 请求、分词、调度、与 GPU Worker 通信、去分词、返回结果。这种设计简单直观,但有一个致命问题:Python 的 GIL(全局解释器锁)让 CPU 工作和 GPU 编排互相阻塞

具体来说:当 API Server 在做分词(CPU 密集)时,调度器无法同时做下一步的调度决策;当去分词在处理输出时,Worker 的结果无法及时被消费。在高并发场景下,CPU 成了瓶颈,GPU 利用率反而下降。

V1 的解法是把 vLLM 拆分为三类进程:

graph TB subgraph "API Server 进程" HTTP["HTTP Handler"] TOK["Tokenizer"] DETOK["Detokenizer"] end subgraph "EngineCore 进程" SCHED["Scheduler"] KV["KV Cache Manager"] end subgraph "Worker 进程(每张 GPU 一个)" W0["Worker 0
GPU 0"] W1["Worker 1
GPU 1"] end HTTP --> |ZMQ IPC| SCHED SCHED --> |共享内存 MQ| W0 SCHED --> |共享内存 MQ| W1 W0 --> |共享内存 MQ| DETOK W1 --> |共享内存 MQ| DETOK style HTTP fill:#8b5cf6,color:#fff,stroke:none style TOK fill:#8b5cf6,color:#fff,stroke:none style DETOK fill:#8b5cf6,color:#fff,stroke:none style SCHED fill:#ec4899,color:#fff,stroke:none style KV fill:#ec4899,color:#fff,stroke:none style W0 fill:#10b981,color:#fff,stroke:none style W1 fill:#10b981,color:#fff,stroke:none
进程 职责 CPU/GPU 通信方式
API Server HTTP 处理、分词、去分词 纯 CPU ZMQ IPC → EngineCore
EngineCore 调度、KV Cache 管理 纯 CPU 共享内存 MQ → Workers
Worker × N 模型前向计算 GPU 共享内存 MQ → API Server

每个进程有自己的 Python 解释器和 GIL,互不干扰。API Server 可以在 Worker 执行 GPU 计算时同时做分词和去分词;EngineCore 可以在 Worker 执行当前步时提前做下一步的调度决策。

进程间通信:ZMQ 与共享内存

进程分离带来了通信开销的问题。V1 选择了两种机制:

  1. ZMQ IPC------用于 API Server 与 EngineCore 之间。ZMQ 是成熟的消息队列库,IPC 模式使用 Unix Domain Socket,延迟在微秒级。请求和响应的数据量不大(Token ID 序列),ZMQ 完全够用。

  2. 共享内存 MessageQueue ------用于 EngineCore 与 Worker 之间。调度结果需要高效广播给所有 Worker,共享内存避免了数据拷贝。MultiprocExecutorvllm/v1/executor/multiproc_executor.py)使用 rpc_broadcast_mq 向所有 Worker 广播调度指令,Worker 通过 worker_response_mq 返回结果。

为什么不全用 ZMQ 或全用共享内存?因为它们各有优势:ZMQ 提供了更好的抽象(发布-订阅、请求-回复模式),适合 API Server 这种面向外部的组件;共享内存则提供了最低的延迟和零拷贝语义,适合引擎内部的高频通信。

V0 到 V1:一张对比表

维度 V0 V1 为什么改
进程模型 单进程 多进程(API Server + EngineCore + Workers) GIL 瓶颈,CPU/GPU 无法并行
调度模型 区分预填充/解码阶段 统一 Token 调度 简化逻辑,天然支持分块预填充
Worker 状态 无状态(每步广播全量) 有状态(只发差量) 减少通信开销
前缀缓存 可选,有性能开销 默认启用,零开销 优化数据结构后开销可忽略
CUDA 图 标准 CUDA 图 分段 CUDA 图(Piecewise) 突破标准 CUDA 图的动态 shape 限制
请求抽象 SequenceGroup Request 消除不必要的复杂性
去分词 在引擎循环内 在 API Server 进程异步执行 CPU 工作不阻塞 GPU 编排

V1 发布后的基准测试显示,在不使用多步调度的情况下,吞吐量提升了 1.7 倍。对于视觉语言模型(VLM),提升更为显著,因为图像编码器的 CPU 预处理在 V0 中会严重阻塞 GPU。

1.3 五大子系统

从全局视角看,vLLM 的代码库可以映射为五个核心子系统。理解它们的职责边界和交互方式,就掌握了整个系统的骨架。

子系统一:入口层(Entrypoints)

vllm/entrypoints/ 目录是 vLLM 面向外部世界的窗口。它包含:

  • OpenAI 兼容 API Server ------最常用的入口,支持 /v1/chat/completions/v1/completions/v1/embeddings 等端点
  • 离线推理接口 ------LLM 类(vllm/entrypoints/llm.py),用于批量推理场景,不启动 HTTP 服务
  • gRPC Server------高性能 RPC 接口
  • CLI 工具 ------vllm serve 命令

入口层的核心原则是------它只负责协议适配和序列化,不包含任何推理逻辑。所有的智慧都在 EngineCore 中。

子系统二:引擎核心(Engine Core)

vllm/v1/engine/vllm/v1/core/ 是 vLLM 的大脑。

  • EngineCorevllm/v1/engine/core.py)------引擎的主循环。接收请求,驱动调度器,分发执行指令,收集结果。它是唯一了解系统全局状态的组件。
  • Schedulervllm/v1/core/sched/scheduler.py)------调度器。决定每一步哪些请求参与计算,每个请求处理多少 Token。
  • KVCacheManagervllm/v1/core/kv_cache_manager.py)------KV Cache 块的分配、释放和共享。

引擎核心的设计哲学是集中决策,分布执行。调度器掌握全局信息(所有请求的状态、所有块的使用情况),做出集中的调度决策;Worker 只需要执行决策,不需要知道其他 Worker 在做什么。

子系统三:执行层(Executor & Worker)

vllm/v1/executor/vllm/v1/worker/ 是 vLLM 的肌肉。

  • Executor 是引擎核心与 Worker 之间的代理层。它屏蔽了 Worker 的部署拓扑------EngineCore 不需要知道 Worker 是单卡还是多卡,是本地进程还是远程 Ray Actor。
  • Worker 是真正操控 GPU 的角色。每个 Worker 负责一张 GPU 卡,执行模型的前向传播。

Executor 有三种实现,对应三种部署模式:

graph LR EC["EngineCore"] --> UNI["UniProcExecutor
单卡"] EC --> MULTI["MultiprocExecutor
多卡(单机)"] EC --> RAY["RayExecutor
多卡(多机)"] UNI --> W1["Worker
GPU 0"] MULTI --> W2["Worker
GPU 0"] MULTI --> W3["Worker
GPU 1"] RAY --> W4["Worker
GPU 0
Node A"] RAY --> W5["Worker
GPU 0
Node B"] style EC fill:#ec4899,color:#fff,stroke:none style UNI fill:#10b981,color:#fff,stroke:none style MULTI fill:#10b981,color:#fff,stroke:none style RAY fill:#10b981,color:#fff,stroke:none

子系统四:模型层(Model Executor)

vllm/model_executor/ 包含了所有与具体模型相关的代码:

  • 模型实现------Llama、Qwen、Mistral、GPT-NeoX 等数百种模型的适配
  • 层实现------注意力层、MLP 层、嵌入层等基础组件
  • 量化支持------FP8、GPTQ、AWQ 等量化方案的实现
  • 模型加载------从 HuggingFace Hub 下载和加载模型权重

模型层的设计原则是可插拔。添加一个新模型只需要实现规定的接口,不需要修改引擎核心或调度器。这种解耦是 vLLM 能支持数百种模型的基础。

子系统五:内核层(Kernels)

vllm/kernels/vllm/vllm_flash_attn/ 包含了 vLLM 的性能秘密------用 CUDA 和 Triton 编写的定制内核。

最核心的是 PagedAttention 内核------它实现了在分页 KV Cache 上高效执行注意力计算。标准的 FlashAttention 假设 KV Cache 是连续的,PagedAttention 内核则能通过块表间接寻址,在非连续的内存块上完成同样的计算。

此外还有 FlashAttention3 的集成------vLLM V1 的主力注意力后端,支持预填充和解码在同一批次内混合执行。

1.4 源码目录导航

最后,让我们快速浏览 vllm/ 目录的组织结构,为后续的源码之旅做好准备:

bash 复制代码
vllm/
├── v1/                     # V1 引擎(当前默认)
│   ├── engine/             #   EngineCore、客户端
│   ├── core/               #   调度器、KV Cache 管理
│   ├── executor/           #   Executor 实现
│   ├── worker/             #   GPU Worker
│   ├── attention/          #   注意力后端
│   ├── sample/             #   采样逻辑
│   └── spec_decode/        #   投机解码
│
├── entrypoints/            # API 服务器、CLI
│   ├── openai/             #   OpenAI 兼容 API
│   └── llm.py              #   离线推理接口
│
├── model_executor/         # 模型实现与加载
│   ├── models/             #   各模型适配
│   ├── layers/             #   基础层(注意力、量化等)
│   └── model_loader/       #   权重加载
│
├── distributed/            # 分布式通信
│   ├── parallel_state.py   #   并行状态管理
│   └── kv_transfer/        #   KV Cache 传输
│
├── kernels/                # CUDA/Triton 内核
├── lora/                   # LoRA 适配器
├── multimodal/             # 多模态输入处理
├── config/                 # 配置类
└── sampling_params.py      # 采样参数

记住两个原则:

  1. V1 的代码在 v1/ 目录下 ------如果你在 vllm/engine/vllm/core/ 中看到类似的代码,那是 V0 遗留的,不要混淆
  2. v1/engine/core.py 开始读------这是整个系统的入口点,从这里出发你可以到达任何一个子系统

1.5 本章小结

本章建立了对 vLLM 的全局认知:

  • 一个推理请求经历六个阶段:HTTP 接收 → 分词入队 → 调度决策 → KV Cache 分配 → GPU 前向计算 → 去分词输出
  • V1 架构采用多进程设计,API Server、EngineCore、Worker 各司其职,通过 ZMQ 和共享内存通信,彻底消除 GIL 瓶颈
  • 五大子系统------入口层、引擎核心、执行层、模型层、内核层------构成了 vLLM 的骨架
  • V1 相比 V0 的核心改进:多进程、统一调度、有状态 Worker、零开销前缀缓存

这张地图将在后续章节中逐步展开。下一章,我们将走进 EngineCore------那个驱动一切运转的心脏。


延伸阅读

  • vLLM V1 Alpha 发布博文:vllm.ai/blog/v1-alp...
  • PagedAttention 论文:Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention", SOSP 2023
  • V1 引擎架构讨论:GitHub Issue #8779
相关推荐
杨艺韬6 小时前
vLLM内核探秘-第15章 多模态推理
agent
杨艺韬6 小时前
vLLM内核探秘-第6章 Worker 与 Executor:GPU 军团
agent
杨艺韬6 小时前
vLLM内核探秘-第11章 分块预填充与混合批处理
agent
杨艺韬6 小时前
vLLM内核探秘-第2章 EngineCore:引擎的心脏
agent
杨艺韬6 小时前
vLLM内核探秘-第17章 API 服务器与生产部署
agent
杨艺韬6 小时前
vLLM内核探秘-第3章 调度器:Token 的交通指挥
agent
杨艺韬6 小时前
vLLM内核探秘-第5章 KV Cache 管理:寸土寸金的显存
agent
杨艺韬6 小时前
vLLM内核探秘-第8章 前向计算与 CUDA Graph
agent
杨艺韬6 小时前
vLLM内核探秘-前言
agent