PagedAttention 论文精读:vLLM 如何用 OS 分页思想把 KV Cache 利用率从 20% 拉到 96%

— SOSP'23 论文逐节拆解 + 面试高频 Q&A
学习笔记 AI Infra 推理引擎 PagedAttention vLLM SOSP'23 Stage 1 / Week 2
目录
  1. 1. 论文概览:一句话说清楚在干什么
  2. 2. 问题:三种内存浪费(§3)
  3. 3. 核心方案:PagedAttention(§4.1)
  4. 4. 为什么分 block 算 attention 数学上等价
  5. 5. KV Cache Manager:block 的生命周期(§4.2-4.3)
  6. 6. 高级场景:Parallel Sampling / Beam Search / Shared Prefix(§4.4)
  7. 7. 调度与抢占(§4.5)
  8. 8. Continuous Batching 与 PagedAttention 的关系
  9. 9. 与 FlashAttention 的关系
  10. 10. 核心实验数据
  11. 11. OS 概念到 vLLM 的完整映射表
  12. 12. ⭐ 学习过程高频卡点答疑(15 个 Q)
  13. 13. 参考资料

1. 论文概览:一句话说清楚在干什么

一句话总结
现有 LLM 推理系统因为 KV Cache 的连续存储要求,导致严重碎片化(有效利用率仅 20.4%),限制了并发批处理大小和吞吐。vLLM 借鉴 OS 虚拟内存分页思想,将 KV Cache 切成固定大小的 block、通过 block table 间接寻址,使利用率达到 96.3%,吞吐提升 2-4×。
项信息
论文标题Efficient Memory Management for Large Language Model Serving with PagedAttention
会议SOSP'23(系统顶会)
一作Woosuk Kwon(UC Berkeley)
通讯Ion Stoica(Spark/Ray 创始人)
arxiv2309.06180
代码github.com/vllm-project/vllm

2. 问题:三种内存浪费(§3)

现有系统将每个请求的 KV Cache 存储在连续内存空间中,按最大可能序列长度预分配。这带来三种浪费:

浪费类型原文术语含义
① 预留槽位Reserved slots为未来可能生成的 token 预留空间。在请求整个生命周期内被独占,不能被其他请求利用。
② 内部碎片Internal fragmentation请求实际生成的 token 数少于最大长度,剩余预分配空间永远不会被使用(只有请求结束后才知道浪费了多少)。
③ 外部碎片External fragmentation不同请求的预分配块大小不同,释放后产生大小不一的空洞。新请求即使总空闲内存够也可能找不到足够大的连续空间。
⚠ 量化数据(必背)
注意区分
"冗余复制"(多序列间无法共享 KV)是论文 §4.3-4.4 讨论的另一个问题,解决手段是 reference counting + CoW。它不属于 §3 定义的三种内存浪费。

3. 核心方案:PagedAttention(§4.1)

核心 idea

将每个序列的 KV Cache 划分为固定大小的 KV block(默认每个 block 存 16 个 token 的 K 和 V),这些 block 在物理显存中不必连续。通过 block table(等价于 OS 页表)记录逻辑 block → 物理 block 的映射。

类比 OS

OS 概念vLLM 对应
物理页 (physical page)物理 KV block
虚拟地址空间序列的逻辑 KV 位置
页表 (page table)block table
按需分页 (demand paging)按需分配 KV block
字节 (byte)token
进程 (process)请求 (request)

PagedAttention Kernel 做了什么

标准 attention kernel 假设 K/V 在内存中连续排列。PagedAttention kernel 多了一步间接寻址:

  1. 查 block table 得到各物理 block 地址
  2. 从不连续的物理地址逐 block 加载 K/V
  3. 逐 block 计算 raw score(q · K_block / √d)
  4. 所有 block 的 score 做全局 softmax
  5. 用 softmax 权重对所有 block 的 V 加权求和

数学结果与标准 attention 完全等价。

4. 为什么分 block 算 attention 数学上等价

论文公式(§4.1 公式 4)

$$A_{ij} = \frac{\exp(q_i^\top K_j / \sqrt{d})}{\sum_{l=1}^{\lceil T/B \rceil} \exp(q_i^\top K_l / \sqrt{d})} , \quad o_i = \sum_{j=1}^{\lceil T/B \rceil} V_j A_{ij}^\top$$

其中 $K_j$ 是第 j 个 block 的所有 Key 向量,$B$ 是 block size。

拆开看(用人话讲)

假设序列 10 个 token,block_size = 4,切成 3 个 block:

Block 0: token 0,1,2,3 的 K/V
Block 1: token 4,5,6,7 的 K/V
Block 2: token 8,9 的 K/V(没填满)

标准 attention 做的事:

scores = [q·k_0, q·k_1, q·k_2, ..., q·k_9] / √d    ← 10 个 score
weights = softmax(scores)                              ← 对 10 个分数归一化
out = Σ weights[i] * v_i                              ← 加权求和

PagedAttention 做的事(等价改写):

# Step 1:逐 block 算 raw score(不做 softmax!)
scores_block0 = [q·k_0, q·k_1, q·k_2, q·k_3] / √d
scores_block1 = [q·k_4, q·k_5, q·k_6, q·k_7] / √d
scores_block2 = [q·k_8, q·k_9] / √d

# Step 2:拼成一条,做一次全局 softmax
all_scores = concat(scores_block0, scores_block1, scores_block2)
weights = softmax(all_scores)      ← 分母是全部 10 个 exp 之和

# Step 3:用全局 weights 对所有 V 加权
out = weights[0:4] · V_block0 + weights[4:8] · V_block1 + weights[8:10] · V_block2
✓ 为什么等价
本质上就是把"遍历所有 token"改写成"遍历所有 block,在每个 block 内遍历 token"。这是纯粹的循环重组,不改变任何数值计算。softmax 的分母是全局的(所有 block 的 exp 之和),不是 block 内部的——这是正确性的关键。
⚠ 常见错误理解
如果对每个 block 单独做 softmax 再拼接——那确实会错(每个 block 内部归一化的基数不一样)。
PagedAttention 不是这么做的。它是分块算 raw score + 局部 exp sum,最后做一次全局归一化。

5. KV Cache Manager:block 的生命周期(§4.2-4.3)

组件结构

Block 生命周期(4 步)

步骤发生什么Block 状态
① 新请求到达(Prefill)prompt 的 KV 一次性计算,按 block_size 填入物理 block。最后一个 block 可能没填满。从 pool 分配 N 个 block
② Decode 生成中新 token 的 KV 填入最后一个 block 的空位最后一个 block 的 #filled + 1
③ 最后一个 block 满了从 pool 申请新的物理 block,block table 追加一行新 block 加入
④ 请求结束(EOS)所有物理 block 归还到 pool全部释放

唯一的内部碎片:最后一个 block 可能没填满(最多浪费 block_size - 1 = 15 个 token 的空间)。整体来看极小——相比之前 80% 的浪费,这点代价几乎可忽略。

6. 高级场景:Parallel Sampling / Beam Search / Shared Prefix(§4.4)

6.1 Parallel Sampling(并行采样)

同一 prompt 生成多个回答时:

6.2 Beam Search

多个 beam 共享不同长度的公共前缀,共享模式随解码动态变化(类似 OS 的进程树)。

CoW 触发条件:某条 beam 要向一个 ref_count > 1 的 block 写入新数据时。

触发后发生什么:

  1. 复制该 block 为新物理 block
  2. 该 beam 的 block table 改指向新 block
  3. 原 block 的 ref_count - 1
  4. 在新 block 上写入新 token

只 copy 被修改的那一个 block,不是整段 KV Cache——这就是 CoW 的核心价值。跟 Unix fork() 完全一致。

6.3 Shared Prefix(共享前缀 / System Prompt)

多用户共享相同 system prompt 时,前缀的 KV block 全局只存一份。新请求直接将逻辑 block 映射到已有物理 block。类似 OS 中进程间共享动态库。

7. 调度与抢占(§4.5)

调度策略

当显存不够时:两种抢占策略

策略做法适合场景
Swapping(交换) 将被抢占请求的 KV block 从 GPU 复制到 CPU 内存。GPU 空闲后再换回。 CPU 内存充裕、KV cache 体量中等
Recomputation(重计算) 直接丢弃被抢占请求的 KV cache。等有资源后重新跑 Prefill(将原始 prompt + 已生成 token 合并重算 KV)。 prompt 短(重算代价低)、CPU 内存紧张
为什么 FCFS 不会饿死短请求?
FCFS 是先到先做,不是长请求优先。如果显存不够:抢占的是最后到达的请求(而不是短请求),保证先到的请求不被后来者挤掉。所以不存在"长请求饿死短请求"的问题——反而是后来的请求可能被暂时 swap/recompute。

8. Continuous Batching 与 PagedAttention 的关系

Continuous BatchingPagedAttention
解决什么调度问题(请求什么时候进出 batch)内存管理问题(KV Cache 怎么存)
核心机制每 decode step 重新组 batch,请求结束立刻换上新的KV Cache 分 block + block table 映射
没有对方能不能工作能(Orca 就是有 CB 没有 PA)能(可以不做 CB 只用 PA 管内存)
但效果差没 PA → 碎片严重 → batch 放不满没 CB → 请求进出不灵活 → GPU 空转
✓ 一句话
Continuous Batching 管"谁进谁出",PagedAttention 管"进来的人住哪"。两者正交、配合使用效果最大。

9. 与 FlashAttention 的关系

FlashAttentionPagedAttention
解决什么单次 attention 计算的 HBM 读写量(IO 效率)多请求 KV Cache 的显存布局(内存管理)
手段Tiling(分块计算 + 不保存中间结果到 HBM)分页(非连续存储 + block table 间接寻址)
优化层面一次 matmul 内部的数据搬运多请求间的内存分配/释放/共享
能一起用吗?能,且 vLLM 实际就是两者结合——PagedAttention kernel 内部采用 FlashAttention 风格的 tiling 减少 HBM 读写

什么是 HBM 读写?

HBM(High Bandwidth Memory)就是 GPU 的主显存("A100 有 80GB 显存"说的就是 HBM)。GPU 的内存层次类似 CPU:

层次GPU 上叫什么大小带宽
寄存器Register File~几 KB/SM最快
L1 / 共享内存SRAM (Shared Memory)每 SM 192KB(A100)~19 TB/s
主显存HBM40-80 GB2 TB/s

关键差距:SRAM 比 HBM 快约 10 倍,但容量小 1000 倍。

"单次 attention 的 HBM 读写"到底指什么

做一次 softmax(Q @ K^T / √d) @ V 计算,标准实现的数据搬运过程:

1. 从 HBM 读出 Q           (T×d)
2. 从 HBM 读出 K           (T×d)
3. 计算 Q @ K^T → score    (T×T)
4. 把 score 写回 HBM        ← SRAM 放不下 T×T 矩阵!
5. 从 HBM 重新读出 score
6. 做 softmax → weights    (T×T)
7. 把 weights 写回 HBM      ← 又放不下
8. 从 HBM 重新读出 weights
9. 从 HBM 读出 V           (T×d)
10. 计算 weights @ V → output
11. 把 output 写回 HBM

总 HBM 读写量:读 3Td + 2T²,写 2T² + Td → 总量 $\mathcal{O}(T^2 + Td)$

当 T=4096, d=128 时:$T^2 = 16M$ 元素,大部分时间 GPU 在等数据从 HBM 搬过来,算力空转。

FlashAttention 如何解决

核心 idea:不把中间结果(score、weights 这两个 T×T 矩阵)写回 HBM。

做法:Tiling(分块计算)——把 Q/K/V 切成小块,每次只在 SRAM 里处理一小块,在 SRAM 内部完成 softmax 的部分计算,中间结果不落地到 HBM。

标准 AttentionFlashAttention
HBM 读写量$\mathcal{O}(T^2 + Td)$$\mathcal{O}(Td)$
T=4096, d=128~34M 元素~2M 元素
加速比基准2-4×(主要是省了 HBM 搬运时间)
✓ 两者叠加
vLLM 的 PagedAttention kernel 内部用 FlashAttention 风格的 tiling:既从不连续 block 读数据(PA 的贡献——省空间),又不把中间结果写回 HBM(FA 的贡献——省带宽)。空间效率 + IO 效率同时拉满。

10. 核心实验数据

对比维度数据
vLLM vs FasterTransformer吞吐高 14-24×
vLLM vs Orca (最优配置)吞吐高 2.2-4.3×
KV Cache 有效利用率vLLM 96.3% vs Orca(Max) 20.4%
Beam Search (width=6)vLLM 比 Orca 吞吐高 2.3×
PagedAttention kernel 额外开销< 5%
最优 block_size16(消融实验 16-32 最优区间)

11. OS 概念到 vLLM 的完整映射表

OS 概念vLLM 对应说明
物理页物理 KV block固定大小(16 token),存储在 block pool 中
虚拟页逻辑 KV block按序列位置编号:0, 1, 2, ...
页表block table每个请求一张,逻辑→物理映射
按需分页按需分配 block最后一个 block 满了才申请新 block
Copy-on-WriteBeam Search / Parallel Sampling 共享ref_count > 1 时写入触发 copy
Swap to diskSwap to CPU memory被抢占请求的 KV block 暂存 CPU
共享动态库Shared Prefix / Prefix Caching多请求共享同一 system prompt 的 KV block
进程请求 (request)独立的逻辑地址空间
字节token寻址的最小单位

12. ⭐ 学习过程高频卡点答疑(15 个 Q)

Q1:PagedAttention 解决的核心问题是什么?
现有系统因 KV Cache 的连续存储要求导致严重碎片化和共享困难,有效利用率仅 20%,限制了并发批处理大小和吞吐。PagedAttention 通过分页管理消除碎片、支持共享,将利用率提升到 96%。
Q2:三种内存浪费?
① Reserved slots:为未来 token 预留的空间,请求期间被独占。② Internal fragmentation:预分配空间中永远不会被用到的部分(请求提前结束)。③ External fragmentation:不同请求释放后留下大小不一的空洞,新请求找不到连续大块。注意:"冗余复制"不在这三种之内。
Q3:Block Table 的作用?对应 OS 什么概念?
记录逻辑 block 到物理 block 的映射 + 每个 block 的填充数。等价于 OS 页表。
Q4:vLLM 利用率 vs Orca(Max)?
96.3% vs 20.4%。
Q5:block_size = 16 意味着什么?为什么不用 1 或 128?
每个 block 存 16 个 token 的 KV。太小→block table 膨胀 + kernel 效率低(warp 利用率低);太大→内部碎片增多。16-32 是消融实验验证的最优区间。
Q6:哪个阶段用到 PagedAttention 的分块读取?
Decode 阶段。Prefill 是一次性并行计算所有位置的 KV(此时是"建立 cache"),不需要从不连续 block 里读历史 KV。Decode 每步要读取全部历史 KV → 需要 PagedAttention kernel 按 block table 寻址。
Q7:请求结束后 KV block 怎么处理?有相同前缀的新请求呢?
归还到全局 block pool。如果有 Prefix Caching:前缀 block 通过 hash 匹配保留在 pool 中不立即释放,新请求命中 hash 直接复用。
Q8:Beam Search 中 CoW 什么时候触发?触发后发生什么?
触发条件:某 beam 要向一个 ref_count > 1 的 block 写入数据。触发后:①复制该 block 为新物理 block ②该 beam 的 block table 改指向新 block ③原 block ref_count - 1 ④在新 block 上写入。只 copy 被修改的那一个 block,不是整段 KV。
Q9:为什么分 block 算再全局 softmax 跟连续算等价?
因为 softmax 分母 = 所有位置的 exp 之和。分 block 算时,每个 block 贡献"局部 exp sum",汇总后得到的全局分母与一次性算完全一致。本质是"把逐 token 遍历改写为逐 block 遍历",纯循环重组,数值零误差。不是对每个 block 单独做 softmax 再拼接(那样确实会错)。
Q10:Continuous Batching 跟 PagedAttention 什么关系?
两者正交。CB 管调度(请求何时进出 batch),PA 管内存(KV 怎么存)。没有 PA 也能做 CB(Orca 就是),但效果差——碎片严重导致 batch 放不满。两者配合才能发挥最大威力。
Q11:block_size=16,请求生成 17 个 token,浪费多少?重要吗?
第 2 个 block 只填了 1 个 token,浪费 15 个 token 的空间。整体不重要:每个请求最多浪费 block_size-1 个 token,相比几百~几千 token 的请求总量,占比极低(<3%)。且所有 block 大小相同→零外部碎片。用极小的内部碎片换来了零外部碎片,trade-off 非常划算。
Q12:PagedAttention kernel 比标准 kernel 多了什么开销?大吗?
多了 block table lookup(间接寻址)。实测额外开销 < 5%,相比之前 80% 的显存浪费可忽略。
Q13:显存不够时两种抢占策略?
① Swapping:把被抢占请求的 KV block 从 GPU 复制到 CPU 内存,有空间后再换回。适合 CPU 内存充裕。② Recomputation:直接丢弃 KV cache,等有资源后重新跑 Prefill。适合 prompt 短(重算代价低)、CPU 内存紧张。
Q14:FCFS 会不会饿死短请求?
不会。FCFS 是先到先做。显存不够时抢占的是最后到达的请求(不是短请求),保证先到的不被饿死。后来的请求可能被暂时 swap/recompute,等资源释放后恢复。
Q15:PagedAttention 跟 FlashAttention 能一起用吗?
能。两者正交:FlashAttention 优化单次 attention 的 HBM 读写(tiling),PagedAttention 优化多请求 KV Cache 的内存布局。vLLM 的 PagedAttention kernel 内部可采用 FlashAttention 风格的 tiling 进一步减少 HBM 读写。

13. 参考资料


这是 Stage 1 / Week 2 的第一篇,对应 AI Infra 转型学习计划"精读 vLLM 论文"任务。