系列:vLLM 与 SGLang 框架解码手记

(二)vLLM 原理与结构:从 PagedAttention 到删掉 PagedAttention

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,…),但在物理显存里可以散落在任意位置。中间由一张块表做映射。

逻辑视图(请求看到的连续序列) tok 0-15 tok 16-31 tok 32-47 tok 48-63 请求 A(64 token) 块表 Block Table [0]→#7 [1]→#2 [2]→#9 [3]→#4

物理显存(block 池,任意散落) #0 空 #1 B #2 A1 #3 空 #4 A3 #5 B #6 空 #7 A0 #8 空 #9 A2 #10 B #11 空

共享前缀:写时复制 A、B 若 prompt 前缀相同 → 指向同一物理块,引用计数 +1

图 1:PagedAttention 的逻辑—物理映射。碎片被压缩到”最多浪费一个 block”,且相同前缀可跨请求共享同一物理块。 结果:KV 有效利用率从 ~20-40% 提到 >90%,同等显存下并发数提升数倍。

1.2 三个直接收益

  1. 碎片几乎消失:浪费的上限是”最后一个 block 里没填满的部分”,最多 15 个 token 的空间,而不是上千。
  2. 共享前缀零成本:系统提示词、few-shot 示例、多轮对话历史——只要前缀相同,多条请求指向同一批物理块,引用计数管理,写时复制。
  3. 并行采样便宜:一个 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(OpenAI 兼容 / 前端进程) tokenize · 请求校验 ZMQ / IPC EngineCore(独立进程,busy-loop) Scheduler 调度器 迭代级调度 · 抢占 chunked prefill · 优先级 KVCacheManager block 池 · 块表 前缀缓存 · 卸载/分层 Structured Output grammar / JSON schema 流式解析引擎 Model Runner V2(MRv2) async-first · 零 CPU-GPU 同步 · 整步 CUDA Graph 捕获 Attention 后端 FlashAttn / FlashInfer 量化 kernel TP / PP / EP

关键设计: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 还在),只是执行路径变直了。

要区分两件事:被删掉的是 vLLM 内部那个叫 PagedAttention 的算子抽象层;分页 KV Cache 这个机制本身依然存在,只是由注意力后端 kernel 直接完成。这是"抽象下沉",不是"功能退化"。

4.2 MRv2:把整步 decode 录成一张图

同一版里 Model Runner V2 转正,解决的是上一篇说的第三个瓶颈:GPU 等 CPU。

MRv2 的核心是 async-first + 零 CPU-GPU 同步:

效果有多大?单 token 的 kernel 启动开销从 ~300 µs 塌缩到 ~5 µs。

MRv1:每步都有同步点 CPU 准备 GPU 前向 D2H CPU 判断 H2D CPU 准备 GPU 前向 … GPU 空闲区间:CPU 准备 + D2H + 判断 + H2D ≈ 单步一半时间

MRv2:整步录成 CUDA Graph,CPU 领先一步 CPU 备 N CPU 备 N+1 CPU 备 N+2 …(CPU 始终领先)

GPU 图 N(一张图) GPU 图 N+1 GPU 图 N+2 GPU 连续无气泡

图 3:kernel 启动开销 ~300 µs → ~5 µs。收益在中小 batch(真实 Agent 流量区间)最明显。

4.3 代价与坑

0.25 是一次大跨度换代,升级必须做完整回归:
  • 自定义算子 / 老架构 GPU 会回退到 MRv1 路径,拿不到收益;
  • Transformers v4 被弃用(#40389),需迁到 v5;
  • 编译要求提到 C++20;
  • 旧的 partial-prefill 参数被移除(#49244)。

紧接着的 v0.25.1(7/14) 是一个两 commit 的必打补丁:

一句话看懂 #48330:融合核省 HBM 往返是真快,但"默认 dtype 一致"这个隐含假设会破。加一个 dtype 哨兵就能兼得速度与正确性——这是所有激进 kernel 融合都要面对的经典权衡。

5. vLLM 的差异化优势

跑了这么多版本跟踪下来,vLLM 最稳的护城河其实是广度:

6. 小结

机制解决什么现状
分页 KV + 块表显存碎片、前缀共享保留;算子层下沉到 attention 后端
连续批处理 + chunked prefill队头阻塞稳定基石
API/Engine 分进程(V1)CPU 前端阻塞 GPU 循环稳定;PD 分离的基础
Model Runner V2GPU 等 CPU 的空转0.25 起所有稠密模型默认
全 CUDA Graph 捕获kernel 启动开销300 µs → 5 µs
Transformers backend parity新模型上线速度Day-0 全速服务

下一篇看 SGLang——它从完全不同的角度切入:不是从”显存怎么管”,而是从”LLM 程序的结构长什么样”出发。

觉得有用?欢迎点赞、收藏,或请作者喝咖啡 ☕️

支付宝收款码

支付宝

微信收款码

微信

💬 留言

评论由 Giscus 驱动(基于 GitHub Discussions)。 当前仓库 NaphJohn/LLM-blog 尚未启用 Discussions:请在 GitHub 仓库 Settings → General → Features 勾选 Discussions 后刷新本页,评论区即自动显示。