理解vLLM的PagedAttention:从KV Cache到Beam Search
在做大语言模型推理优化时,很多人一开始会把注意力集中在算力上,比如 FlashAttention、量化、算子融合等。但在真实的在线服务里,一个同样关键、甚至更容易先把系统拖慢的问题其实是 KV cache 的管理。
vLLM 的 PagedAttention 之所以重要,不是因为它重新发明了 attention 的数学公式,而是因为它重新设计了 KV cache 在显存里的组织方式。这件事直接影响吞吐、延迟,以及服务能同时容纳多少并发请求。
这篇文章会重点解释:
- 为什么传统 KV cache 管理方式容易浪费显存
PagedAttention到底“paged”在什么地方- 它为什么对
beam search这种共享前缀、后续分叉的场景特别友好
先看问题:推理阶段真正膨胀的是KV Cache
在自回归生成中,每生成一个新 token,模型都会把这一时刻每一层的 K 和 V 保存下来,后续 token 做 attention 时直接复用这些历史结果,而不是每一步都从头再算一遍。
这就是 KV cache。
KV cache 的几个特点决定了它很难管:
- 它会随着输出 token 增长而持续变大
- 不同请求的长度差异很大
- 请求会动态进入和退出
- 采样、beam search、并行候选生成等策略会带来“共享前缀,局部新增”的模式
如果用最朴素的方式管理 KV cache,常见做法是:
- 给每个请求分配一段连续显存
- 这段空间通常要预留到某个最大长度
- 请求继续生成时,如果空间不够,要么提前分配很大一块,要么重新搬迁数据
这会导致两个问题。
1. 外部碎片严重
连续内存分配最怕长度不规则。假设有三个请求:
- 请求A最终生成 128 token
- 请求B最终生成 2048 token
- 请求C最终生成 256 token
如果系统为了避免扩容搬迁,往往会给它们保守地预留较大的连续空间。结果就是很多空间虽然已经分配出去了,但实际上没有被有效使用。
2. 共享前缀场景很浪费
比如在 beam search 中,前 100 个 token 可能完全一样,只是在第 101 个 token 开始分叉。如果每个 beam 都复制整份前缀 KV cache,那么显存开销会近似按 beam 数线性膨胀。
这在工程上很亏,因为这些前缀本来就是相同的。
PagedAttention 的核心想法:不要把KV Cache当成一整块连续内存
PagedAttention 的关键思路可以用一句话概括:
把每个序列的 KV cache 拆成固定大小的 block,用逻辑地址到物理 block 的映射来组织,而不是要求整个序列占用一段连续显存。
如果你熟悉操作系统,可以把它类比为“页表 + 物理页框”,但这里只是一个帮助理解的类比,它并不是把 GPU 显存真的交给操作系统做分页。
更具体一点:
- 一个序列的 token 不再要求连续存放
- 每若干个 token 组成一个 block,比如 16 个 token 一个 block
- 模型在计算 attention 时,先根据当前位置找到对应的 block table
- 再通过 block table 找到真正存放
K/V的物理 block
这样一来,序列在增长时只需要:
- 已有 block 继续写
- 当前 block 写满后,再申请一个新的 block
而不是搬迁整段历史 KV。
一个直观例子:连续内存 vs 分块内存
假设 block 大小是 4 个 token,一个请求已经生成了 10 个 token。
如果按 PagedAttention 管理,它的逻辑布局可能是:
1 | 逻辑token位置: [0 1 2 3] [4 5 6 7] [8 9] |
而物理显存里的 block 可能根本不连续:
1 | block table: |
对使用者来说,它仍然像是一条完整的历史序列;但对底层内存管理来说,它已经变成了一组离散的、固定大小的块。
这个设计的收益很直接:
- 只会在最后一个 block 里有少量未用空间
- 序列增长时追加新 block 即可
- 请求结束后,回收的是若干标准大小的 block,更容易复用
为什么这能显著减少显存浪费
我们可以把浪费分成两类来看。
1. 预留浪费减少了
传统连续分配常常需要“按最大可能长度预留”,否则后续扩容就可能触发搬迁。PagedAttention 不需要一次把路铺到头,只要按 block 逐步增长。
也就是说,它把“预测未来要多大”这个难题,变成了“当前只多申请一个标准块”。
2. 碎片的上界变小了
对于每个序列,理论上的尾部浪费最多只会接近一个 block。
假设 block 大小是 16 token,那么一个请求哪怕只用了 17 个 token,也只是:
- 第一个 block 用满 16
- 第二个 block 用了 1 个
浪费的上界就比较清晰,通常远小于“给每个请求预留一大段连续空间”的策略。
这不是“只换了个存储格式”,attention kernel 也要跟着变
如果 KV 不再连续存放,那么 attention kernel 不能再假设“从某个起始地址线性扫过去”。
它必须做到:
- 根据请求的 block table 找到历史 token 对应的物理 block
- 在 block 内定位具体 token
- 把这些分散的
K/V组合起来参与 attention 计算
所以 PagedAttention 真正的难点并不只是“分块存”,而是:
在非连续物理布局下,仍然高效地执行 attention。
这也是为什么 vLLM 的价值不只是“提出了一个抽象”,而是把这个抽象做成了能高吞吐跑在 GPU 上的实现。
Beam Search 是理解 PagedAttention 的一个特别好的例子
下面我们用 beam search 来看 PagedAttention 的优势。
先看beam search的行为模式
假设输入 prompt 是:
1 | 请解释一下为什么分布式系统需要幂等性 |
模型先生成了一段共同前缀,比如:
1 | 在分布式系统中,幂等性之所以重要,是因为 |
此时如果 beam size = 4,那么接下来系统会维护 4 条候选路径。关键点在于:
- 分叉之前的前缀完全相同
- 分叉之后,每个 beam 只是在尾部继续增长
也就是说,4 个 beam 的 KV cache 并不是“4 份完全独立的大对象”,而是:
- 一大段共享前缀
- 再加上各自少量新增 token
传统做法的问题
如果每个 beam 都复制一整份前缀 KV cache,那么前缀越长,浪费越大。
举个简单的数量级例子:
- 公共前缀长度:1024 token
- beam size:4
- 每个 beam 后续各自新增:64 token
如果完全复制,那么 KV cache 大致对应:
1 | 4 * (1024 + 64) |
而真正有必要独立保存的,其实主要只是分叉后的那 64 token。
PagedAttention 怎么做得更聪明
在 PagedAttention 的 block 设计下,beam 分叉时可以让多个 beam 共享同一批前缀 block,只在后续写入时追加自己的 block。
可以把它想象成:
1 | Beam 0 -> [B0][B1][B2][B3] + [C0][C1] |
其中:
B0~B3是共享前缀 blockC/D/E/F才是各个 beam 独有的增量 block
这时显存占用更接近:
1 | shared_prefix + 4 * delta |
而不是:
1 | 4 * (shared_prefix + delta) |
用一个更具体的block例子说明
假设:
- block 大小 = 16 token
- 公共前缀长度 = 48 token
- 每个 beam 在分叉后再生成 10 token
那么公共前缀会占 3 个 block:
1 | [0..15] [16..31] [32..47] |
如果 beam size = 4:
- 传统复制思路:4 个 beam 都各自持有这 3 个前缀 block
PagedAttention思路:这 3 个 block 只存一份,4 个 beam 的 block table 都指向它们
随后每个 beam 追加自己的 token:
- 第 49 到 58 个 token 写到各自新增的 block 里
这时共享关系大概是:
1 | Beam 0 table -> P0 P1 P2 A0 |
其中 P0/P1/P2 是共享 block。
这类“共享前缀 + 尾部增量”的模式,正是 PagedAttention 特别擅长处理的场景。
为什么这对在线服务吞吐很重要
很多人第一次看 PagedAttention 会觉得,这主要是在“省显存”。但在线推理里,省显存本身就会转化成吞吐。
原因很简单:
- 显存浪费少了,同一张卡上能同时容纳更多请求
- 更少的数据搬迁意味着调度更平滑
- 共享前缀能力让复杂解码策略没有那么“奢侈”
所以 PagedAttention 带来的不是一个孤立的小优化,而是:
更高的显存利用率 -> 更大的有效 batch -> 更高的系统吞吐。
这也是 vLLM 在 serving 场景里表现突出的关键原因之一。
需要注意的几个边界
当然,PagedAttention 也不是没有代价。
1. 间接寻址本身会增加实现复杂度
连续内存最简单,分页块表一定更复杂。为了把这套间接寻址的成本降下来,kernel 实现必须非常讲究访存模式、并行粒度和 block 布局。
2. block 太大或太小都不好
如果 block 太大:
- 单序列尾部浪费会增加
如果 block 太小:
- block table 更长
- 元数据管理更重
- kernel gather 开销可能更高
所以 block size 是一个工程权衡,不是越小越好。
3. 它优化的是“服务中的注意力执行与KV管理”
PagedAttention 不会改变模型参数量,也不会神奇地把所有推理问题都解决掉。首 token 延迟、采样策略、网络开销、调度策略、量化误差,这些仍然是完整系统里要分别处理的问题。
我对PagedAttention的一个总结
如果只用一句话概括 PagedAttention,我会这样说:
它把 LLM 推理中最难管理的那部分状态,也就是不断增长的 KV cache,从“必须连续摆放的大对象”变成了“可共享、可追加、可复用的固定块集合”。
这个变化听起来像底层实现细节,但它对系统行为的影响非常大:
- 降低显存碎片
- 避免频繁搬迁 KV cache
- 让 beam search 这类共享前缀场景真正高效
- 提升同卡并发和整体吞吐
从这个角度看,vLLM 的 PagedAttention 更像是一个“推理系统设计”创新,而不只是一个单独的 attention 小技巧。
如果后面继续展开,我觉得还有两个方向很值得写:
PagedAttention和 continuous batching 是怎么配合的- prefix sharing / copy-on-write 在实际 serving 框架里是怎么落地的
这两个主题基本就是把“为什么 vLLM 快”继续往里拆开的下一层。








