文章目录
-
- [1. 先给结论:vLLM 本质上解决的是什么问题](#1. 先给结论:vLLM 本质上解决的是什么问题)
- [2. 从最小例子开始:一个离线引擎到底包含什么](#2. 从最小例子开始:一个离线引擎到底包含什么)
- [3. 引擎启动时,真正重的事情发生在哪里](#3. 引擎启动时,真正重的事情发生在哪里)
-
- [3.1 Init device](#3.1 Init device)
- [3.2 Load model](#3.2 Load model)
- [3.3 Initialize KV cache](#3.3 Initialize KV cache)
- [4. 请求进入引擎后,vLLM 是怎么跑起来的](#4. 请求进入引擎后,vLLM 是怎么跑起来的)
- [5. vLLM 为什么能快:关键在调度器,而不是"批量大"](#5. vLLM 为什么能快:关键在调度器,而不是“批量大”)
-
- [5.1 两类完全不同的工作负载](#5.1 两类完全不同的工作负载)
- [5.2 调度器在干什么](#5.2 调度器在干什么)
- [6. PagedAttention 到底解决了什么](#6. PagedAttention 到底解决了什么)
- [7. 连续批处理为什么重要](#7. 连续批处理为什么重要)
- [8. 五个必须理解的高级特性](#8. 五个必须理解的高级特性)
-
- [8.1 Chunked Prefill](#8.1 Chunked Prefill)
- [8.2 Prefix Caching](#8.2 Prefix Caching)
- [8.3 Guided Decoding](#8.3 Guided Decoding)
- [8.4 Speculative Decoding](#8.4 Speculative Decoding)
- [8.5 Disaggregated P/D](#8.5 Disaggregated P/D)
- [9. 从单 GPU 到多 GPU:为什么需要 MultiProcExecutor](#9. 从单 GPU 到多 GPU:为什么需要 MultiProcExecutor)
- [10. 从单机推理到在线服务:vLLM 的外层服务栈怎么搭](#10. 从单机推理到在线服务:vLLM 的外层服务栈怎么搭)
-
- [10.1 Headless 节点在做什么](#10.1 Headless 节点在做什么)
- [10.2 API 节点在做什么](#10.2 API 节点在做什么)
- [10.3 一个请求的完整生命周期](#10.3 一个请求的完整生命周期)
- [11. 性能指标到底该怎么看:不是只盯 tokens/s](#11. 性能指标到底该怎么看:不是只盯 tokens/s)
-
- [11.1 延迟和吞吐为什么会冲突](#11.1 延迟和吞吐为什么会冲突)
- [12. vLLM 官方是怎么做 benchmark 的](#12. vLLM 官方是怎么做 benchmark 的)
- [13. 这篇文章最值得带走的 6 个认知](#13. 这篇文章最值得带走的 6 个认知)
-
- 1) LLM 推理优化首先是系统问题,不只是算子问题 LLM 推理优化首先是系统问题,不只是算子问题)
- 2) Prefill 和 Decode 是两种不同世界 Prefill 和 Decode 是两种不同世界)
- 3) KV Cache 是推理系统的基础设施 KV Cache 是推理系统的基础设施)
- 4) "持续批处理"比"静态大 batch"更贴近真实线上环境 “持续批处理”比“静态大 batch”更贴近真实线上环境)
- 5) 高吞吐的代价是架构复杂度 高吞吐的代价是架构复杂度)
- 6) 性能优化永远要和业务指标绑在一起 性能优化永远要和业务指标绑在一起)
- [14. 谁最应该读原文](#14. 谁最应该读原文)
- [15. 延伸阅读](#15. 延伸阅读)
- [16. 最后的话](#16. 最后的话)
很多人用 vLLM,停留在两层认知:
-
它很快
-
它支持很多大模型部署特性
但如果你继续往下问一句: 它到底为什么快,内部到底是怎么组织起来的?
答案就不再是一个命令行参数,而是一整套围绕 调度、KV Cache、连续批处理、多进程执行 和 分布式服务 搭起来的系统工程。
这篇文章是对 vLLM 官方长文 Inside vLLM: Anatomy of a High-Throughput LLM Inference System 的中文本地化整理。
我不做逐段直译,而是按中文技术读者更容易吸收的方式,重组为一条从单机引擎到多机服务的理解路径。
原文作者是 Aleksa Gordic,分析基于 commit 42172ad,聚焦 vLLM 的 V1 engine。如果你希望建立对现代 LLM 推理系统的整体心智模型,这篇非常值得读。
1. 先给结论:vLLM 本质上解决的是什么问题
vLLM 要解决的不是"让模型跑起来",而是:
-
在有限显存下,让更多请求同时被服务
-
在交互延迟和整体吞吐之间做更优平衡
-
把单卡、单机、多卡、多机、在线服务串成统一架构
你可以把它理解为一个分层系统:
-
最内层是
LLM Engine / Engine Core -
中间层是
Scheduler + KV Cache Manager + Model Executor
-
外层是
Async Client + Load Balancer + API Server -
再往外才是你熟悉的 OpenAI 兼容接口和服务部署命令
也就是说,vLLM 的重点从来不只是"模型推理",而是高吞吐、低浪费、可扩展的推理系统设计。
2. 从最小例子开始:一个离线引擎到底包含什么
原文先从一个最简单的离线推理例子讲起:
python
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
这个例子看起来很简单,但背后已经有一整套引擎结构在工作。
作者给出的核心拆分是:
-
vLLM config:模型、缓存、并行策略等全部配置项 -
processor:把原始输入转成引擎请求 -
engine core client:与 Engine Core 通信 -
output processor:把底层输出转成用户看到的结果
其中最关键的是 Engine Core。它内部至少有三块:
-
Scheduler -
Model Executor -
KV Cache Manager
如果你只记一句话,可以记这个:
vLLM 的性能,很大程度不是来自"模型本身",而是来自对请求调度和 KV Cache 的管理方式。
3. 引擎启动时,真正重的事情发生在哪里
很多人以为 LLM(...) 只是"加载模型"。
其实在 vLLM 里,初始化阶段已经做了大量系统级准备工作。
3.1 Init device
这一步会做:
-
绑定 CUDA 设备
-
检查 dtype 是否可用
-
根据
gpu_memory_utilization评估显存空间 -
初始化分布式配置
-
创建
model_runner -
创建
InputBatch
简单说,它不是只把模型丢到 GPU,而是在为后续高吞吐执行搭好缓冲区、索引结构和运行环境。
3.2 Load model
这部分比较直观:
-
实例化模型结构
-
加载权重
-
model.eval() -
视情况执行
torch.compile()
3.3 Initialize KV cache
这一段才是重点中的重点。它会:
-
计算每层 KV cache 的规格
-
做一次 profiling forward
-
估算显存里到底能放下多少 KV blocks
-
分配 KV cache tensor
-
绑定到 attention 层
-
预热并捕获 CUDA Graph
也就是说,vLLM 启动时并不是只在"准备推理",而是在预构建一个可复用的高效执行环境 。
这也是为什么它后面能把内核启动开销、KV Cache 命中率和批处理效率做得比较激进。
4. 请求进入引擎后,vLLM 是怎么跑起来的
generate() 的主线非常清晰,可以概括为两步:
-
把请求喂进引擎
-
循环执行
step()
每个请求进入系统时,会经历:
-
生成 request id
-
对 prompt 做 tokenize
-
封装成
EngineCoreRequest -
进入调度器的
waiting队列
而后续真正反复执行的是 step()。
每次 step 都包含三件事:
-
Schedule:这一步要跑哪些请求 -
Forward pass:真正过模型
Postprocess:写回 token、判停、释放资源
这三个阶段看起来普通,但基本就是整个 vLLM Engine 的最小运行循环。
5. vLLM 为什么能快:关键在调度器,而不是"批量大"
原文里一个非常重要的点是:
vLLM 的核心优势,不是简单把请求拼成一个 batch,而是区分不同阶段的请求,并动态混合调度。
5.1 两类完全不同的工作负载
推理系统里主要有两类请求阶段:
-
Prefill:对整段 prompt 做前向计算 -
Decode:每次只生成一个新 token
它们的性能特征非常不同:
-
Prefill通常更偏compute-bound -
Decode通常更偏memory-bandwidth-bound
为什么?
因为 decode 每次虽然只算一个 token,但仍然要把大模型权重搬出来参与计算。
这意味着,调度器不能把两者当成同一种东西处理。
vLLM V1 的一个重要改进,就是能在同一个 step 中更灵活地混合 prefill 和 decode,而不是像旧版本那样非此即彼。
5.2 调度器在干什么
调度器会优先看 running 队列里的 decode 请求,然后再处理 waiting 里的 prefill 请求。
每次调度都要考虑:
-
当前 token budget 还剩多少
-
每个请求这一步要分配多少新 token
-
需要分配多少 KV blocks
-
空闲 block 够不够
这里就引出一个核心方法:allocate_slots
它大致做三件事:
-
算出需要多少 KV blocks
-
检查 block pool 是否还有足够空间
- 真的把 block 分给请求
所以你可以把调度器理解成一个"请求编排器",而 KV Cache Manager 更像"显存页分配器"。
6. PagedAttention 到底解决了什么
vLLM 最广为人知的关键词之一就是 PagedAttention。
但如果只停留在"这是一个很厉害的注意力优化",其实并没有真正理解它。
更容易理解的方式是:
PagedAttention 的核心不是改写注意力公式,而是把 KV Cache 的内存管理做成类似分页系统。
也就是说,token 对应的 KV 不再要求必须连续地、大块地分配在显存里,而是被映射到一个个 block 上。
这样做的好处是:
-
显存利用率更高
-
碎片更少
-
更容易支持动态请求混合
-
更容易回收和复用 KV Cache
KV Cache Manager 会维护一个 free_block_queue,里面是所有可用的 KV blocks。
请求需要缓存时,从里面取;请求结束时,再把 blocks 还回去。
这就是为什么作者会说:
KV Cache Manager 是 PagedAttention 的心脏。
7. 连续批处理为什么重要
如果你只做离线推理,一次性把 prompts 全部喂进去,问题还相对简单。
但在线服务不是这样:
-
新请求会随时进来
-
老请求还在继续 decode
-
每个请求长度都不同
这时如果你还坚持"一个固定 batch 从头跑到尾",资源利用会很差。
vLLM 的做法是 continuous batching,也就是连续批处理:
-
每个 step 之后都重新考虑当前系统里有哪些请求
-
新请求可以随时加入
-
老请求继续占用自己的 KV Cache
-
所有序列在前向时会被拼成一个"大序列"处理
这就是为什么它不需要传统意义上的右侧 padding 批处理,而是把不同请求压到一个统一执行流中。
这套机制跟 PagedAttention 配合起来,才真正构成了 vLLM 的高吞吐基础。
8. 五个必须理解的高级特性
原文后半部分进入高级能力。这里我建议你重点抓五个概念。
8.1 Chunked Prefill
问题背景很简单:
如果有一个超长 prompt,一次 prefill 可能会占满整个 step,导致其他请求全部排队。
Chunked prefill 的思路就是把长 prompt 切块处理。
你可以把它理解成:
-
不让单个超长请求独占整个引擎时间片
-
用多次 step 完成长 prompt 的 prefill
-
在吞吐和延迟之间做更平衡的调度
对于真实线上服务,这一点很重要,因为长 prompt 一旦不加控制,很容易拖垮整体交互体验。
8.2 Prefix Caching
这是另一个非常实用的特性。
如果多个请求共享相同前缀,就没必要反复重新计算同样的 token。
vLLM 的做法是:
-
把前缀按 block 切分
-
对完整 block 计算哈希
-
把 block hash 映射到缓存中的 KV block
-
后续遇到同样前缀时直接复用
本质上就是:
不要重复做已经做过的 prefill。
这对共享 system prompt、长上下文模板、多轮 agent 工作流都非常有价值。
8.3 Guided Decoding
如果你需要 JSON、语法约束、结构化输出,就会遇到 guided decoding。
这里的核心思路是:
-
每一步解码时,不让所有 token 都参与采样
-
而是先根据语法状态机构造一个合法 token mask
-
把不合法 token 的 logits 直接打成负无穷
这样模型最终只能生成符合目标结构的结果。
作者这里提到,vLLM 会通过类似 xgrammar 的后端来完成语法编译与 mask 维护。
所以 guided decoding 本质上不是"采样技巧",而是把形式语言约束融入解码循环。
8.4 Speculative Decoding
这是近两年推理加速里很重要的一条线。
它的基本思想是:
-
先让一个更轻量的 drafter 猜几个 token
-
再让大模型一次性验证这些 token
-
如果猜对了,就等于一次大模型前向拿回多个 token
原理上,它是在保持最终分布一致的前提下,尽量减少"大模型一 token 一 forward"的低效过程。
vLLM V1 里重点支持的提案方式包括:
-
n-gram -
EAGLE -
Medusa
这块如果你做高并发推理优化,非常值得重点看。
8.5 Disaggregated P/D
这里的 P/D 指的是 Prefill / Decode 分离。
这是一个很有系统味道的设计:
-
Prefill 更偏算力密集
-
Decode 更偏显存带宽敏感
既然两者资源特征不同,那就没必要强绑在同一执行实例里。
于是就有了:
-
Prefill worker 专注做前缀计算
-
Decode worker 专注做逐 token 生成
-
中间通过 KV Cache 服务交换缓存
这样做的好处是,你可以分别优化 TTFT 和 ITL,而不是把一切问题都扔给一个统一实例硬扛。
9. 从单 GPU 到多 GPU:为什么需要 MultiProcExecutor
当模型太大,单卡装不下时,问题就进入并行执行阶段。
此时 vLLM 的核心执行器会从 UniProcExecutor 过渡到 MultiProcExecutor。
它负责的事情可以概括为:
-
按
world_size拉起多个 worker 进程 -
给每个 worker 配置通信管道
-
广播执行任务
-
收集各 worker 结果
从调用者角度看,接口几乎没变,依然是 execute_model。
但在内部,已经从"单进程直接调 worker"变成了"父进程协调多个 GPU worker 并做跨进程通信"。
这也是一个很典型的系统工程思想:
对上层接口尽量保持稳定,把复杂性吸收在执行层内部。
10. 从单机推理到在线服务:vLLM 的外层服务栈怎么搭
很多人看到 vllm serve 就以为事情结束了。
但原文真正精彩的地方,是把在线服务的内部路径讲得非常具体。
10.1 Headless 节点在做什么
在多节点场景下,headless server 节点负责:
-
启动多个 EngineCore 进程
-
每个进程内部再维护输入线程、输出线程和主循环
-
与前端 API 节点握手
-
等待调度与执行请求
也就是说,它更像"纯算力后端"。
10.2 API 节点在做什么
API server 节点上会跑:
-
AsyncLLM -
DPLBAsyncMPClient -
DPCoordinator -
FastAPI / Uvicorn 接口
这里的关键不只是"收请求",而是:
-
选择把请求发给哪个 engine replica
-
跟踪各 replica 的负载
-
异步收集输出
-
把最终结果流式或非流式返回给用户
也就是说,在线服务层本质上是一层:
异步前端 + 负载均衡 + 分布式执行协调器
10.3 一个请求的完整生命周期
原文最后把一次 /v1/completions 请求的路径完整串起来了。
压缩成中文,就是这条链:
-
请求打到 FastAPI 路由
-
路由层做 tokenize 和 metadata 封装
-
AsyncLLM.generate()把请求交给异步客户端 -
负载均衡逻辑选一个合适的 engine
-
请求进入对应 engine 的 input socket
-
engine 主线程反复调用
step()
-
输出线程把结果发回前端
-
API server 再把响应回给调用方
这样你会发现:
用户看到的是一个 HTTP 接口,但系统内部其实是异步任务、消息队列、进程编排、GPU worker 和缓存管理共同完成的一条流水线。
11. 性能指标到底该怎么看:不是只盯 tokens/s
如果你只用一个指标评价推理系统,通常会犯错。
原文把几个核心指标讲得很清楚:
-
TTFT:首 token 延迟 -
ITL:相邻 token 间延迟 -
TPOT:平均每输出 token 的时间 -
E2E Latency:端到端完成时间 -
Throughput:吞吐 -
Goodput:满足 SLO 的有效吞吐
为什么一定要区分这些指标?
因为在线交互和离线批处理的优化目标完全不同:
-
交互产品更关心 TTFT 和 ITL
-
离线任务更关心整体 Throughput
而这两者往往互相拉扯。
11.1 延迟和吞吐为什么会冲突
作者用 roofline 视角解释了一个常见现象:
-
batch 很小时,单请求 ITL 低,但总吞吐不高
-
batch 增大后,吞吐上升,但每一步的执行时间也可能上升
尤其 decode 阶段经常是带宽受限,而不是纯算力受限。
所以"多塞点请求进去"不一定总是更优,关键在于你想优化哪类指标。
这也是为什么 vLLM 里会有:
-
continuous batching
-
chunked prefill
-
prefll/decode 分离
-
speculative decoding
这些看似分散的特性,本质上都在服务同一个目标:
更精细地调和延迟与吞吐。
12. vLLM 官方是怎么做 benchmark 的
原文还提到 vLLM 自带的 benchmark 能力:
bash
vllm bench latency
--model <model-name>
--input-tokens 32
--output-tokens 128
--batch-size 8
此外还有:
-
vllm bench throughput -
vllm bench serve
三者分别偏向:
-
测延迟
-
测离线吞吐
-
模拟真实在线服务负载
这也提醒我们一件事:
没有脱离场景的"推理性能"。
你必须先明确自己是在做:
-
聊天机器人
-
agent 调度
-
批量生成
-
数据清洗
-
合成数据生产
然后再看应该重点关心哪一组指标。
13. 这篇文章最值得带走的 6 个认知
读完这篇 vLLM 长文,我觉得最值得保留的不是具体类名,而是下面这 6 个系统认知:
1) LLM 推理优化首先是系统问题,不只是算子问题
真正影响线上表现的,往往不是一个 kernel,而是:
-
请求怎么排
-
KV Cache 怎么管
-
新老请求怎么混
-
多卡多机怎么协同
2) Prefill 和 Decode 是两种不同世界
它们计算特征不同,指标诉求不同,最优调度方式也不同。
理解这一点,才容易理解为什么会有 chunked prefill、spec decode、P/D 分离。
3) KV Cache 是推理系统的基础设施
PagedAttention、prefix caching、外部 KV transfer,本质上都围绕 KV Cache 展开。
谁把 KV Cache 管得好,谁就更容易做出高吞吐系统。
4) "持续批处理"比"静态大 batch"更贴近真实线上环境
线上请求是流式进入的,不会乖乖排队等你凑齐一个完美 batch。
所以 continuous batching 才是更现实的优化方向。
5) 高吞吐的代价是架构复杂度
从 UniProc 到 MultiProc,再到 DP 协调和 API server,复杂度是层层叠加的。
vLLM 的价值之一,就是把这份复杂度尽可能封装掉。
6) 性能优化永远要和业务指标绑在一起
如果你的目标是聊天体验,就别只盯总 tokens/s。
如果你的目标是离线吞吐,也别为了首 token 漂亮数字牺牲整体产能。
14. 谁最应该读原文
我觉得这篇最适合三类人:
-
正在做 LLM 推理平台或模型服务的工程师
-
想深入理解 vLLM / SGLang 等推理框架内部设计的人
-
已经会部署模型,但对"为什么这样设计"还没有完整心智模型的人
如果你只是想"把服务跑起来",官方文档可能已经够用。
但如果你想真正理解现代 LLM serving 是如何从单机脚本演化成分布式推理系统的,这篇原文非常值得精读。
15. 延伸阅读
-
原始博客:Inside vLLM: Anatomy of a High-Throughput LLM Inference System
-
vLLM 项目地址:vllm-project/vllm
-
V1 Engine 指南:vLLM V1 Guide
如果你后续还想继续往深处读,原文中尤其建议重点延伸这几个主题:
-
PagedAttention
-
Prefix Caching
-
Speculative Decoding
-
Disaggregated Prefill / Decode
-
Multi-GPU 与 Multi-Node Serving
16. 最后的话
这篇文章最打动我的,不是它列了多少特性,而是它把一个现代推理系统拆得非常诚实:
-
先从最小离线引擎讲起
-
再进入调度、缓存和执行
-
然后一路扩展到多进程、多卡、多机和在线服务
-
最后回到性能指标与 benchmark
这样的写法非常适合建立全局视角。
如果你最近正好在看推理框架,或者在做自己的 LLM serving 栈,我会建议你把这篇当成一篇"系统地图"来读。
哪怕你暂时不需要记住所有类名,只要抓住它背后的设计逻辑,也会对后续很多工程判断很有帮助。