1. PagedAttention:把操作系统的虚拟内存搬进 KV Cache
上一篇提到,朴素推理的 KV Cache 是”按最大长度连续预留”,利用率只有 20%–40%。vLLM 的第一个杀手锏就是解决这个问题,思路直接借鉴了操作系统。
1.1 类比
| 操作系统 | vLLM |
|---|---|
| 进程的虚拟地址空间 | 一条请求的逻辑 KV 序列 |
| 物理内存页(4 KB) | KV block(通常 16 个 token) |
| 页表(page table) | 块表(block table) |
| 按需分页、写时复制 | 按需分块、前缀共享 |
一条请求的 KV 在逻辑上是连续的(token 0,1,2,…),但在物理显存里可以散落在任意位置。中间由一张块表做映射。
1.2 三个直接收益
- 碎片几乎消失:浪费的上限是”最后一个 block 里没填满的部分”,最多 15 个 token 的空间,而不是上千。
- 共享前缀零成本:系统提示词、few-shot 示例、多轮对话历史——只要前缀相同,多条请求指向同一批物理块,引用计数管理,写时复制。
- 并行采样便宜:一个 prompt 要采 n 个不同回答(best-of-n),prompt 部分的 KV 只存一份。
2. 连续批处理:从”等整批”到”等一步”
分页解决了显存,还要解决队头阻塞。
vLLM 的调度粒度不是”一批请求”,而是一次迭代(iteration-level scheduling):
每一步 decode 结束后:
├─ 谁生成完了 → 立刻返回、释放它的 block
├─ 队列里有新请求 → 立刻塞进空出来的槽位
└─ 显存不够 → 抢占(把某条请求的 KV 换出或重算)
于是批的组成是动态流动的,GPU 永远在满负荷跑活跃请求,而不是等最慢的那一条。这就是 Continuous Batching(也叫 in-flight batching)。
配合 chunked prefill(把长 prompt 的 prefill 切成小块,穿插进 decode 步)后,长 prompt 不再一次性霸占整步计算,正在流式输出的请求也不会被卡住。
3. V1 架构:进程分层
vLLM V1(2025 年重构)把整个引擎拆成清晰的层次:
关键设计:API Server 与 EngineCore 分进程。前端的 tokenize、HTTP 解析、JSON 序列化这些 CPU 密集活儿不再阻塞 GPU 调度循环,两边通过 ZMQ 通信。这也是后来 PD 分离能自然长出来的基础。
4. 转折:0.25 为什么把 PagedAttention 删了
这是 2026 年 7 月最值得说的一件事。
vLLM v0.25.0(7/11-7/12 发布) 做了一件反直觉的事:移除 PagedAttention(PR #47361),同时把 Model Runner V2 变成所有稠密模型的默认路径(#39337)。
看起来像是自废武功,其实是抽象层过期了。
4.1 为什么能删
PagedAttention 最初是一个”中间层”:因为当时的注意力 kernel 只会读连续的 KV,所以 vLLM 需要额外一层来做逻辑—物理转换。
但到了 2026 年,现代注意力后端(FlashAttention 3/4、FlashInfer)已经在 kernel 内部原生支持读 block table 并 gather 分页 KV。也就是说,“分页”这件事本身已经下沉进了 kernel。
那么 vLLM 里那层 PagedAttention 抽象就变成了纯粹的冗余开销——多一次 Python 侧的调度、多一层张量整形、多一批不必要的同步。删掉它,分页语义不变(块表还在、KVCacheManager 还在),只是执行路径变直了。
PagedAttention 的算子抽象层;分页 KV Cache 这个机制本身依然存在,只是由注意力后端 kernel 直接完成。这是"抽象下沉",不是"功能退化"。
4.2 MRv2:把整步 decode 录成一张图
同一版里 Model Runner V2 转正,解决的是上一篇说的第三个瓶颈:GPU 等 CPU。
MRv2 的核心是 async-first + 零 CPU-GPU 同步:
- 前向路径上所有需要”把结果拷回 CPU 判断一下”的地方全部消除,改成 GPU 上就地处理;
- 因为没有 host 同步点了,整个 decode step(包括投机解码的 draft + verify)可以被捕获成一张完整的 CUDA Graph;
- step N 和 step N+1 可以重叠——CPU 提前一整步准备下一步的输入。
效果有多大?单 token 的 kernel 启动开销从 ~300 µs 塌缩到 ~5 µs。
4.3 代价与坑
- 自定义算子 / 老架构 GPU 会回退到 MRv1 路径,拿不到收益;
- Transformers v4 被弃用(#40389),需迁到 v5;
- 编译要求提到 C++20;
- 旧的 partial-prefill 参数被移除(#49244)。
紧接着的 v0.25.1(7/14) 是一个两 commit 的必打补丁:
- #48330 混合 dtype 量化融合守卫——修 NVFP4 模型静默输出乱码。根因很值得看:FlashInfer 的
allreduce + RMSNorm + static-quant三合一融合核,在激活是 BF16、RMSNorm 权重是 FP32 时 dtype 不一致,把 4-bit NVFP4 读成了错误位模式,隐状态损坏,输出变成重复的!!!!!。修复方式是加一个 dtype 匹配哨兵:dtype 不一致走安全路径,一致时保留融合。 - #47888 TorchCodec 缺 FFmpeg 时不再阻塞启动。
5. vLLM 的差异化优势
跑了这么多版本跟踪下来,vLLM 最稳的护城河其实是广度:
- 模型广度:
Transformers backend parity(0.25 起 Transformers 后端速度追平原生实现),意味着只要 HF 上有实现的新架构,Day-0 就能全速服务,不用等 vLLM 写原生实现。 - 硬件广度:CUDA / ROCm(AITER)/ Intel XPU(DeepSeek-V4
fuse_index_qSYCL 路径)/ TPU / CPU,是所有引擎里最全的。 - 量化广度:FP8 / INT4 / AWQ / GPTQ / NVFP4 / MXFP4 / compressed-tensors 全支持。
- 生态广度:KV Connector 接口(可对接 Mooncake / NIXL / LMCache)、Rust router、二级 KV 缓存 TieringManager(PR #42285)。
6. 小结
| 机制 | 解决什么 | 现状 |
|---|---|---|
| 分页 KV + 块表 | 显存碎片、前缀共享 | 保留;算子层下沉到 attention 后端 |
| 连续批处理 + chunked prefill | 队头阻塞 | 稳定基石 |
| API/Engine 分进程(V1) | CPU 前端阻塞 GPU 循环 | 稳定;PD 分离的基础 |
| Model Runner V2 | GPU 等 CPU 的空转 | 0.25 起所有稠密模型默认 |
| 全 CUDA Graph 捕获 | kernel 启动开销 | 300 µs → 5 µs |
| Transformers backend parity | 新模型上线速度 | Day-0 全速服务 |
下一篇看 SGLang——它从完全不同的角度切入:不是从”显存怎么管”,而是从”LLM 程序的结构长什么样”出发。
💬 留言
NaphJohn/LLM-blog尚未启用 Discussions:请在 GitHub 仓库 Settings → General → Features 勾选 Discussions 后刷新本页,评论区即自动显示。