对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 24 · Mon 4/13 · Guest Lecture by Woosuk Kwon(Inferact): Efficient LLM Inference with Paged Attention and vLLM(slides:llmsys-24-vLLM_woosuk_kwon.pdf,82 页)。 阅读材料:Kwon et al. (2023) Efficient Memory Management for Large Language Model Serving with PagedAttention(SOSP);vLLM 文档与源码。 依据说明:slides 分两部分。PagedAttention:LLM 推理又慢又贵、推理过程、顺序依赖与批处理、KV 缓存及其大小、“高效管理 KV 缓存是高吞吐的关键”(13B 模型在 A100-40GB 上的显存分布、批大小与吞吐)、MoE 模型的稀疏度、过去系统的预留与碎片、只有 20%–40% 的 KV 显存被利用、PagedAttention 与操作系统分页、KV 块、块表、在块上算注意力(5%–10% 的内核开销)、按需分配的逐步演示、内部碎片、并行采样的共享与写时复制、束搜索、共享省下的显存、块不够时的抢占(换出 / 重算)、与操作系统虚拟内存的对照、吞吐结果、成为行业标准。vLLM:API(离线的 LLM 类、OpenAI 兼容的服务器)、四类优化——CPU 开销(Rust 的 API 服务器、异步调度、异步去分词、在 GPU 上准备输入、CUDA 图:整图与分段图)、GPU 内核(FlashInfer、torch.compile 融合、DeepSeek V3.2 的例子)、模型并行(数据 / 张量 / 专家 / 上下文 / 流水线并行、预填充与解码分离、混合并行、Kimi K2.5 的案例)、显存管理(混合模型的显存分配器)。本讲义的块式 KV 缓存与分页推理引擎、在块上算注意力的实现、并行采样 / 束搜索 / 前缀缓存的实验、抢占的实验与换出 / 重算的时间模型、带显存约束的服务模拟、CUDA 图的玩具模型,都是本讲义自己设计的,不代表课堂上讲了什么;请求长度的分布是本讲义假设的,不是 ShareGPT 的真实统计。 配套代码 Code:
code/lecture24/(5 个脚本 +paged.py;复用第 19 讲的模型、第 22 讲的前向;都在 3 秒以内) 预计阅读时间:3 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 解释为什么过去的推理系统只用上了 20%–40% 的 KV 缓存显存:预留、内部碎片、外部碎片各是怎么来的。
- 说清楚 PagedAttention 的三件东西:KV 块、块表、按需分配;以及注意力内核怎样在不连续的块上计算。
- 用引用计数和写时复制解释并行采样、束搜索、前缀缓存怎样共享 KV。
- 说出块不够时的两种恢复方式(换出、重算)的代价,以及 vLLM 为什么选择重算 + FCFS。
- 把 PagedAttention 与操作系统的虚拟内存逐项对照。
- 列举 vLLM 作为一个真实推理引擎的其他优化:CPU 开销(异步调度、CUDA 图)、内核、并行、混合模型的显存分配。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 推理服务器、连续批处理、RadixAttention(第 22 讲) | 预填充与解码分离(第 26 讲) |
| KV 缓存与 Flash-Decoding、在线 softmax(第 9、21 讲) | KV 缓存的分层存储与传输(第 27 讲) |
| 虚拟内存与分页(前置讲 P4);MoE 与专家并行(第 17 讲) | 混合注意力模型(Gated DeltaNet 等)的细节 |
0.3 本讲地图 Map
§1 推理为什么又慢又贵:顺序依赖、批处理、KV 缓存 ← 代码 01
§2 过去的 KV 管理:预留、内部碎片、外部碎片 ← 代码 01
§3 PagedAttention:KV 块、块表、按需分配、在块上算注意力、块大小 ← 代码 02、01
§4 共享:并行采样的写时复制、束搜索、前缀缓存 ← 代码 03
§5 抢占与恢复:换出 vs 重算;与操作系统的对照 ← 代码 04
§6 效果:吞吐 ← 代码 05
§7 vLLM:CPU 开销、内核、并行、混合模型的显存 ← 代码 05
§8 小结 §9 常见误区 §10 练习 §11 延伸阅读 §12 下一讲预告
1. 推理为什么又慢又贵 · Why LLM inference is slow and expensive
slides 第 3–4 页:LLM 通常跑在 H100 这样的高端 GPU 上,可是每块 GPU 只能服务每秒寥寥几个请求,带推理过程的简单问题也可能要几分钟。而且问题越来越难:模型更大、上下文更长、每个问题生成的 token 更多。
推理过程(slides 第 5–7 页):输入提示,模型一次生成一个 token,把它接到输入后面,重复直到达到最大长度或生成结束符。这种顺序依赖让 GPU 很难并行(第 8 页);唯一的出路是批处理——把很多请求放在一起算,充分利用并行硬件(第 9 页)。而批处理的上限由 KV 缓存决定:每个 token 在每一层都有一对 K、V 向量,后面的 token 都要用到它们(第 10–11 页)。KV 缓存很大:每个 token 约 1 MB,一个完整的请求要几个 GB。
2. 过去的 KV 管理:预留与碎片 · KV cache management before PagedAttention
2.1 显存都去哪了
slides 第 12–13 页(2023 年的常见配置):13B 模型在 A100-40GB 上,参数占 26 GB(65%),留给 KV 缓存的只有约 12 GB(30%)。01 号脚本第 1 部分:OPT-13B 每个 token 的 KV 是 2 × 40 层 × 5120 维 × 2 字节 = 800 KiB,12 GB 只能放约 14,648 个 token。
| 做法 | 同时能运行的请求 |
|---|---|
| 每个请求按最大长度 2048 预留 | 7 |
| 只存真实的 token(生成过程中平均每个请求约 312 个) | 约 47 |
slides 的图:过去的系统批大小 8,PagedAttention 40;吞吐从 0.8 提到 3.2 请求 / 秒。高效管理 KV 缓存是高吞吐服务的关键——这是整篇论文的出发点。
slides 第 14 页补充了一个今天更迫切的理由:MoE 模型。DeepSeek-V3 每个 token 激活 256 个专家里的 8 个(稀疏度 32 倍),Kimi K2 是 384 个里的 8 个(48 倍)。一个专家每步只分到批里 1/32–1/48 的 token;要让专家的矩阵乘也有足够大的批,批要大 32–48 倍(05 号脚本第 2 部分:每个专家 64 个 token 就需要批里有 2048–3072 个 token),KV 缓存的需求就更大了。
2.2 三种浪费
过去的系统沿用深度学习里”输入输出形状固定”的习惯:为每个请求预先分配一段连续的显存,长度按它可能的最大长度算(slides 第 15 页)。这带来三种浪费:
- 预留:为将来的输出预留的位置,现在还空着(之后会用上)。
- 内部碎片:输出长度事先不知道,按最大长度预留;请求实际用不了那么多,剩下的永远用不上。
- 外部碎片:每个请求预留的大小不同,释放之后留下大小不一的空洞,新请求可能”总空闲够、却找不到一段连续的空间”。
01 号脚本第 2 部分看某一时刻 40 个正在生成的请求:
| 预留方式 | 真实 token | 为将来的输出预留 | 内部碎片 |
|---|---|---|---|
| Orca(Max):预留 2048 | 14.3% | 7.2% | 78.4% |
| Orca(Pow2):预留 2 的幂 | 41.7% | 21.0% | 37.3% |
| Orca(Oracle):恰好预留最终长度(做不到) | 66.5% | 33.5% | 0 |
| 分页(块 16) | 97.8% | 0 | 2.2% |
持续服务时(第 3 部分的模拟,1000 个请求、每秒 4 个):有效利用率 Max 20.1%、Pow2 39.5%、Oracle 60.3%。vLLM 论文实测已有系统只有 20.4%–38.2% 的 KV 显存存着真实的 token(slides 第 16 页:“20%–40%”)。模拟里外部碎片挡住过 Pow2 的 270 个、Oracle 的 350 个请求(Max 每次预留的大小相同,空洞总能被下一个请求用上,所以它的浪费全在内部)。
3. PagedAttention · Paging the KV cache
3.1 借用操作系统的分页
slides 第 17 页:PagedAttention 是应用层的分页与虚拟化,对象是注意力的 KV 缓存。操作系统把进程的虚拟地址空间切成页、映射到任意的物理页;PagedAttention 把每个请求的 KV 缓存切成KV 块(固定数量的 token 的 K、V),映射到任意的物理块:
- KV 块(第 18 页):一块固定大小的显存,从左到右存若干个 token 的 KV(例子里块大小 4)。
- 块表(第 19 页):每个请求一张,“逻辑块 i → 物理块号”,外加最后一块”填了几个”。逻辑上连续的块在物理上可以不连续。
- 按需分配(第 22–26 页):最后一块填满了、要写下一个 token 时,才分配一个新的物理块。
02 号脚本第 1 部分在块大小 4 的缓存里交替生成两个请求 A、B:预填充后 A 占物理块 0–2、B 占 3–7;解码到第 4 步时 A 的最后一块满了,分到物理块 8,紧接着 B 分到块 9——两个请求的物理块交错在一起。浪费的只有每个请求最后一块里还没填的位置。
3.2 在块上算注意力
slides 第 20–21 页:注意力内核按块表逐块取出不连续的 K、V,当场计算;不先把它们拼成连续的张量(那又是一次读写)。块内算分数,块之间用在线 softmax 合并——第 21 讲 FlashAttention / Flash-Decoding 的同一个技巧。paged.paged_attention_decode 就是这样写的;02 号脚本第 2 部分把它接进一步真实的解码,与”先拼成连续的 KV 再算”相比,logits 只差舍入(块大小 4 / 8 / 16:1.9e-6 / 8.3e-7 / 4.8e-7)。
代价是间接寻址:每块先查表、再去一个不连续的地址读。slides:GPU 内核的时延因此慢 5%–10%——但换来的是批大小大好几倍。真实的 PagedAttention 是专门的 CUDA 内核,可以和 FlashAttention 结合。
把分页缓存接进推理引擎(第 3 部分,16 个请求):输出 16/16 与单独生成相同;峰值只占 90 块(360 个 token 的空间),而按最大长度预留要 1024、“预知每个请求最终长度”也要 462——短请求结束、释放了块之后,长请求才增长到最终长度。
3.3 内部碎片与块大小
slides 第 27 页:内部碎片只发生在每个序列的最后一块,每个序列浪费的 token 少于块大小;没有外部碎片(块大小相同,任何空闲块都能用)。序列约 1000 个 token、块约 10 个 token 时,浪费不到 1%;相对过去的系统是 2.5–5 倍的改进。
块大小怎么选(01 号脚本第 4 部分,平均每个序列 313 个 token):块 1 / 16 / 64 / 256 的内部碎片 0% / 2.3% / 8.9% / 29.0%。块越小,块表越长,内核的间接寻址越多、每次读的连续数据越少(GPU 读小块效率低),换出时的拷贝也越零碎(§5)。vLLM 默认块大小 16。
4. 共享 · Sharing KV blocks
分页的另一个好处是共享变得容易:两个序列的块表指向同一个物理块就是共享,块上记一个引用计数。
4.1 并行采样与写时复制
slides 第 28–30 页:一个提示采样多个回答(并行采样),所有样本的提示部分完全相同。预填充只做一次,然后让所有样本的块表指向同一批提示块(引用计数 = n)。提示的最后一块通常没填满;某个样本第一次往里写自己的 token 时,发现这一块被共享,就先复制一份再写——写时复制(copy-on-write),和操作系统 fork 进程时的做法一样。除了最后一块,所有提示块都在样本之间共享。
03 号脚本第 1 部分(30 个 token 的提示、每个回答 12 个 token):
| n | 峰值块数(共享) | 不共享时 | 省下 | 写时复制次数 |
|---|---|---|---|---|
| 2 | 15 | 22 | 32% | 1 |
| 4 | 23 | 44 | 48% | 3 |
| 8 | 39 | 88 | 56% | 7 |
n 个样本只复制 n − 1 次——最后一个写的样本发现引用计数已经是 1,直接写。每个样本用自己的随机数生成器,回答与各自独立生成完全相同。
4.2 束搜索:进程树的 fork 与 kill
slides 第 31 页:束搜索的几个束共享很长的前缀,每一步分叉出新的候选、淘汰落选的束——“像进程树的 fork 和 kill”。用分页实现:新束 fork 自它的父束(共享全部块),落选的束 free(引用计数减一,降到 0 的块回到空闲列表),只有写进共享块时才复制。03 号脚本第 2 部分(10 步):
| 束宽 | 峰值块数(共享) | 不共享 | 省下 |
|---|---|---|---|
| 2 | 11 | 18 | 39% |
| 4 | 15 | 36 | 58% |
| 6 | 20 | 54 | 63% |
束越宽,共享越多;结果与不共享完全相同。vLLM 论文的实测也是束搜索省得比并行采样多得多(slides 第 32 页)。
4.3 跨请求的前缀缓存
同样的机制可以在不同请求之间共享相同的前缀(系统提示、少样本示例)——第 22 讲用基数树做这件事。vLLM 的自动前缀缓存用另一种方式:给每个填满的块算一个哈希,哈希里串进前面所有块的哈希,所以”哈希相同”就意味着”整个前缀相同”;新请求从第一块开始查表,命中多少块就复用多少块。03 号脚本第 3 部分(12 个请求共用 30 个 token 的系统提示):块哈希在块大小 1 / 4 / 16 时命中 78.6% / 74.6% / 61.0%,逐 token 的基数树 78.6%。粒度是一块:共同前缀不是块大小的整数倍时,最后那半块不能复用。两种做法殊途同归——基数树是显式的前缀树,块哈希链是隐式的前缀树。
5. 抢占与恢复 · Preemption and recovery
5.1 块不够了
分页之后不再为输出预留空间,这是”乐观”的:大家都在增长,总有某一步某个请求要不到新块(slides 第 34–35 页)。办法是抢占:释放某些请求的 KV,让其他请求先跑完(第 36 页)。两种恢复方式:
- 换出(swapping):把被抢占请求的块搬到 CPU 内存,之后再搬回 GPU。
- 重算(recomputation):直接丢掉,之后把”提示 + 已经生成的 token”当作新的提示重新预填充。
slides 第 37 页的几点:
- 抢占以整个请求为单位——解码每一步都要用到它之前所有 token 的 KV,只留一部分没有意义。
- 换出:块越小,拷贝越零碎,开销越大。
- 重算出奇地快:所有 token 的 KV 可以并行计算(就是一次预填充)。
- vLLM 的策略:重算 + FCFS(先来的先保证完成,抢占最后到达的)。
04 号脚本第 1 部分把引擎的物理块调小(12 个请求、块大小 4,如果每个请求都一直占着最终需要的块要 105 块):35 块时抢占 3 次,重算的方式多算了 72 个 token,换出的方式搬了 288 KiB 到 CPU 再搬回来;两种方式的输出都与单独生成逐 token 相同。
5.2 换出还是重算
第 2 部分的时间模型(OPT-13B、A100,一个 256 token 的请求,KV 共 200 MiB;PCIe 有效带宽 25 GB/s、每次拷贝 10 μs 的固定开销都是本讲义的假设):
| 块大小 | 1 | 4 | 16 | 256 |
|---|---|---|---|---|
| 换出 + 换入 | 21.9 ms | 18.1 ms | 17.1 ms | 16.8 ms |
| 重算(屋顶线下限) | 21.3 ms |
在这个模型里两者同一量级,块越小换出越贵。重算还有别的好处:不占 CPU 内存、不和其他传输争 PCIe、被抢占的请求重新预填充时能和别的请求的解码放在同一批里分摊读权重的时间。真实的取舍要看硬件和负载(vLLM 论文图 19 是实测)。
5.3 与操作系统的对照
slides 第 38 页:
| 操作系统 | PagedAttention / vLLM |
|---|---|
| 页 | KV 块 |
| 页表(多级) | 块表(单级:表比 KV 数据小得多) |
| 进程 | 请求 |
| 进程之间共享页、fork 时写时复制 | 样本 / 束之间共享 KV 块、写时复制 |
| 按页换出到磁盘 | 按请求抢占;可以换出,也可以丢掉之后重算 |
| 分页消除外部碎片 | 同样 |
最大的不同:KV 缓存永远可以由 token 重新算出来,所以”恢复”多了一个操作系统没有的选项——重算。
6. 效果 · Results
slides 第 39–41 页(OPT-13B、A100-40GB):
- 贪心解码(ShareGPT 负载):2.4 倍的吞吐提升(平均每批的请求数大幅增加);
- 束搜索(Alpaca 负载,相对 Orca(Pow2)):不用束搜索 1.8 倍,束宽 2 / 4 / 6 时 2.4 / 3.2 / 3.5 倍——束越宽,共享越多;
- PagedAttention 已经成了行业标准:TensorRT-LLM、HuggingFace TGI、Fireworks AI 等都采用了它。
05 号脚本第 1 部分的模拟(OPT-13B、A100-40GB、12 GB KV、请求长度是假设的分布):
| 方式 | 最大吞吐(请求 / 秒) |
|---|---|
| Orca(Max) | 1.16 |
| Orca(Pow2) | 1.89 |
| Orca(Oracle) | 2.56 |
| vLLM(分页) | 3.94 |
vLLM 是 Orca(Max) 的 3.4 倍、Pow2 的 2.1 倍;到达率 4 个/秒时,vLLM 的平均时延 9.2 秒,Orca(Max) 已经积压到 306 秒。根本原因就是平均批大小:6.9 → 28.9。
7. vLLM:一个真实的推理引擎 · vLLM in practice
7.1 API 与规模
vLLM 是由很多机构共同开发的开源推理引擎(slides 第 43 页,76K 星)。两种用法(第 44–45 页):离线批量推理的 LLM 类(llm.generate(prompts)),以及 vllm serve <模型> 启动的 OpenAI 兼容服务器。slides 把它的优化分成四类(第 47 页):CPU 开销、GPU 内核、模型并行、显存管理与缓存。
7.2 CPU 开销
为什么 CPU 开销重要(第 49 页):推理的一步只有 5–10 ms(每个请求每秒 100–200 个 token),而训练的一步是 100 ms 到 1 秒以上。05 号脚本第 3 部分:一步 5 ms 时,1 ms 的串行 CPU 开销就损失 17%;一步 100 ms 时只损失 1%。在 Python 里写出 1 ms 的开销”太容易了”。vLLM 的对策(第 50–54 页):
- API 服务器用 Rust 重写;
- 异步调度:提前一步排好、准备好下一批,CPU 的工作与 GPU 的执行重叠,GPU 不会因为 CPU 而停下(与第 22 讲的零开销调度、NanoFlow 同一个思路);
- 异步去分词;
- 在 GPU 上准备输入:批处理、分页注意力、采样参数的大量簿记过去用很多小的 PyTorch 运算在 CPU 上做,现在用 Triton 内核在 GPU 上做——又快又干净,还让异步调度和推测解码可以一起用(GPU 上的准备内核能直接看到拒绝采样的结果);
- CUDA 图。
CUDA 图(第 55–62 页):eager 模式下 CPU 每启动一个内核都要走一遍 Python / PyTorch,内核很短时 GPU 在等 CPU——这部分开销能占到时延的 50%。CUDA 图把整串内核录下来一次提交。但整图要求形状固定、执行中不能有 CPU 操作,而推理有很多动态的东西:任意混合的预填充与解码、运行时选择的注意力内核(例如共享前缀的 Cascade Attention)、CPU 卸载。vLLM 的折中是分段 CUDA 图:用 torch.compile 在注意力处把模型切开,注意力之外那些按 token 独立、无状态的运算录成图,注意力留在 eager 模式里,想怎么写都行。实测(第 62 页,H100、输入 4K、输出 50):比 eager 快 6%–39%,最坏比整图慢 2%–7%,批大小 ≥ 8 时只慢 0%–2%。05 号脚本第 4 部分的玩具模型显示了同样的趋势(批大小 1 时分段图比整图慢 2%,批大了以后三者一样快),只是它的数字是凑的。
7.3 GPU 内核
第 64–67 页:复杂或性能关键的内核(注意力、MoE)用 FlashInfer 等库,vLLM 提供 AttentionBackend、FusedMoE 这样的抽象来接入不同的实现;访存受限的小内核(RMSNorm、RoPE)用 torch.compile 自动融合,或者手写融合内核。slides 用 DeepSeek V3.2 的例子展示了融合前后的内核序列。
7.4 模型并行
第 69–78 页:模型越来越大(Kimi K2.5 有 1T 参数,量化后约 600 GB),一块卡放不下;多卡还能叠加算力和带宽。两个目标:少通信(NVLink 1.8 TB/s,跨机的 InfiniBand 只有约 50 GB/s)、负载均衡(系统常常被最慢的部件拖住)。几种并行在推理里的取舍:
- 数据并行:复制多个引擎,负载均衡器按负载、KV 显存、前缀缓存分请求——不通信,但不省参数显存、不降时延。
- 张量并行:切线性层的权重(第 16 讲)——能降时延,KV 缓存也能切;通信多,通常要 NVLink;切分数受 KV 头数的限制。
- 专家并行:专家分到不同的卡(第 17 讲)——对内核友好、通信比张量并行少;只用于 MoE 层,注意力要配别的并行;负载可能不均。
- 上下文并行:把一条很长的序列切开,交换 Q 或 KV(Ring Attention)——平衡 KV 缓存;不省参数。
- 流水线并行:层分到不同的卡——通信最少,但时延增加、阶段之间可能不均。
- 预填充与解码分离(第 76 页):分开部署,独立地控制首 token 时延和 token 间时延、各自优化——第 26 讲。
实际部署是混合并行。slides 的 Kimi K2.5(NVFP4、GB200,8K 输入、1K 输出)案例:高吞吐配置是分离部署(预填充 10 组 4 卡的数据 + 专家并行,解码 1 组 24 卡的数据 + 专家并行),低时延配置是不分离、4 卡的张量 + 专家并行。
7.5 混合模型的显存分配
第 80–81 页:现代 LLM 越来越多地用混合结构:全注意力层与滑动窗口注意力层或 Gated DeltaNet 这类线性注意力层交替(GPT-OSS、Qwen 3.5)。不同类型的层需要的”KV”形状完全不同(滑动窗口层只要最近 W 个 token,线性注意力层每个序列只有一个固定大小的状态)。静态地把显存池按层的类型切开会造成严重的碎片;vLLM 用动态划分:各类型的分配器共用一个显存池,并调整分配的粒度(块大小)让它们对齐,所有开源模型的浪费在 0%–12%。
8. 小结 · Summary
- 吞吐取决于批大小,批大小取决于 KV 缓存的显存。 过去按最大长度预留连续空间,预留、内部碎片、外部碎片让只有 20%–40% 的 KV 显存存着真实的 token。
- PagedAttention 把 KV 缓存分页:固定大小的块、每个请求一张块表、用到才分配;注意力内核在块上直接算(慢 5%–10%)。内部碎片只在最后一块,没有外部碎片,利用率接近 100%。
- 分页让共享变得容易:引用计数 + 写时复制,并行采样、束搜索、跨请求的前缀都能共享 KV 块。
- 块不够时抢占整个请求:换出到 CPU 或丢掉之后重算;vLLM 选择重算 + FCFS。
- 与操作系统虚拟内存一一对应,唯一的新选项是”重算”。
- 一个真实的推理引擎还要压 CPU 开销(异步调度、GPU 上准备输入、分段 CUDA 图)、接入好内核、支持各种并行、为混合模型管理显存。
9. 常见误区 · Common pitfalls
- “PagedAttention 让注意力算得更快。” 恰恰相反,间接寻址让内核慢 5%–10%;它省的是显存,换来更大的批、更高的吞吐。
- “分页之后就没有浪费了。” 每个序列的最后一块仍有内部碎片(平均半块),块越大越多。
- “块越小越好。” 块太小,块表变长、内核和换出都变成大量的小访问,效率下降;vLLM 默认 16。
- “共享的块可以直接写。” 被多个序列共享(引用计数 > 1)的块必须先复制再写,否则会改掉别人的 KV。
- “抢占时只换出一部分块就行。” 解码每一步都需要全部历史 KV,抢占以请求为单位。
- “重算一定比换出慢。” 重算是一次并行的预填充,可以和别的请求放在一个批里;在我们的时间模型里两者同一量级。
- “按块哈希的前缀缓存和基数树一样细。” 块哈希只缓存填满的块,粒度是一块。
- “推理的 CPU 开销无所谓。” 一步只有几毫秒,1 ms 的串行开销就是十几个百分点。
10. 练习 · Exercises
- LLaMA-3-8B(32 层、8 组 KV、头维度 128、FP16)每个 token 的 KV 多大?块大小 16 时一块多大?
- 一个请求的提示 100 个 token、最终输出 300 个 token,最大长度 2048。在它生成到第 150 个 token 时,Orca(Max)、Orca(Pow2)、Orca(Oracle)、分页(块 16)各占多少个 token 的空间?其中多少是真实的?
- 为什么 Orca(Max) 没有外部碎片,Orca(Pow2) 却有?
- 并行采样 n 个样本、提示 L 个 token(L 不是块大小 B 的整数倍),写时复制会发生几次?如果 L 恰好是 B 的整数倍呢?
- 束搜索里,一个束被淘汰时它的块一定会被释放吗?
- 按块哈希的前缀缓存里,为什么第 i 块的哈希要把前面所有块的哈希串进来,而不是只哈希这一块的 token?
- 一个请求有 1000 个 token 的 KV,块大小 16。换出时要拷贝几次?如果每次拷贝有 10 μs 的固定开销、PCIe 25 GB/s、每个 token 128 KiB,一次换出要多久?
- vLLM 抢占”最后到达的请求”。如果改成抢占”已经生成最多 token 的请求”,有什么好处和坏处?
- 推理一步 8 ms,CPU 调度 2 ms。阻塞式和异步调度的吞吐之比是多少?
- 为什么分段 CUDA 图把注意力留在 eager 模式,而不是把注意力也录进图里?
参考答案
- 2 × 32 × 8 × 128 × 2 = 131,072 字节 = 128 KiB / token;一块 16 个 token = 2 MiB。
- 真实的都是 250 个 token。Max:2048;Pow2:最终长度 400 → 512;Oracle:400;分页:⌈250/16⌉ × 16 = 256。
- Max 的每次预留都是同样的 2048,任何一个释放出来的空洞都恰好放得下下一个请求;Pow2 的预留大小各不相同,小空洞放不下大请求,就出现”总空闲够、却没有一段连续的放得下”。
- n 个样本共享提示的最后一块(没填满),前 n − 1 个写的样本各复制一次,最后一个直接写:n − 1 次。L 恰好是 B 的整数倍时,最后一块已经满了,每个样本的第一个新 token 都写进各自新分配的块,不需要复制:0 次。
- 不一定。它的块可能还被别的束共享(引用计数 > 1),free 只是把引用计数减一;只有降到 0 的块才回到空闲列表。
- 同样的 16 个 token 出现在不同的位置、前面跟着不同的内容时,KV 是不同的(注意力看到的上下文不同,RoPE 的位置也不同)。把前缀的哈希串进来,“哈希相同”才意味着”从开头到这一块的内容完全相同”,KV 才能复用。
- 1000/16 = 62.5 → 63 块,63 次拷贝。固定开销 0.63 ms;数据 1000 × 128 KiB ≈ 131 MB,131 MB / 25 GB/s ≈ 5.2 ms;共约 5.9 ms。
- 好处:腾出的块最多。坏处:它投入最多,重算的代价最大,而且离完成可能最近;也破坏了”先来的先完成”的公平性,可能让长请求反复被抢占、饿死。
- 阻塞式每步 10 ms,异步调度每步 max(8, 2) = 8 ms,吞吐之比 1.25。
- 注意力是推理里最”动态”的部分:每个请求的长度、块表、预填充和解码的混合每一步都在变,还可能在运行时选择不同的内核、做 CPU 操作;录成图要求这些都固定。注意力之外的运算按 token 独立、无状态,形状只取决于批里的 token 数,容易录图。把注意力留在 eager 模式,只多了每层一次启动的开销。
11. 延伸阅读 · Further reading
- PagedAttention / vLLM:Kwon et al. (2023);vLLM 的文档、博客(V1 引擎、异步调度、分段 CUDA 图、混合显存分配器)。
- FlashInfer:Ye et al. (2025),可定制的注意力内核库;Cascade Inference(共享前缀的批量解码)。
- NanoFlow:Zhu et al. (2024)。
- S-LoRA:Sheng et al. (2024),把分页的思想用在多 LoRA 适配器的显存上。
- 操作系统:任何一本操作系统教材的虚拟内存、写时复制、换页章节(前置讲 P4)。
12. 下一讲预告 · Next lecture
到这里,训练系统(第 14–18 讲)和推理系统(第 21–24 讲)都讲完了。可是今天最重要的训练流程之一——基于人类反馈的强化学习(RLHF)——同时需要两者:它要用策略模型生成回答(推理),用奖励模型、参考模型、价值模型打分(推理),再训练策略和价值模型(训练),四个模型、三种负载交替进行。第 25 讲讲 ReaLHF:怎样给每一步选不同的并行策略、在步与步之间重新分配参数,让 GPU 不再空转。