PagedAttention
一句话结论:PagedAttention 主要让 KV Cache 放得更紧、分得更灵活、共享得更容易;它通常不是让单次 Attention 计算变少,而是让同一块 GPU 能同时服务更多请求。
先看直观解释
可以把 GPU 显存想成一个仓库。每个推理请求都在不断产生 KV Cache,但系统事先不知道它最终会生成多少 Token。
传统连续分配通常面临两种选择:
- 按最大长度预留。 请求可能只生成几百个 Token,却提前占下可容纳几千个 Token 的连续空间,大量显存长期闲置。
- 随长度动态扩容。 请求增长后可能找不到紧邻空间,只能重新分配、复制,或者被散落的小空洞阻塞。
PagedAttention 把仓库划成固定大小的格子。请求需要更多空间时再领取新格子;这些格子不必相邻,只需用一张表记录它们的顺序。
假设一个 KV Block 容纳 16 个 Token,请求实际使用 600 个 Token:
- 按 4096 Token 上限预留,需要保留 4096 个 Token 的空间;
- 按 Block 分配,只需 $\lceil 600/16 \rceil=38$ 个 Block,即 608 个 Token 的空间;
- 额外浪费只出现在最后一个未填满的 Block 中。
再看真实机制
KV Cache 为什么难管理
自回归推理分为两个主要阶段:
- Prefill 处理 Prompt,为各层、各 Token 生成 K/V。
- Decode 每轮生成新 Token,追加它的 K/V,并让新 Query 读取历史 K/V。
设模型层数为 $L$,KV Head 数为 $H_{kv}$,每个 Head 的维度为 $d$,每个元素占 $s$ 字节。忽略对齐和元数据后,每个 Token 的 KV Cache 大小约为:
$$
M_{token}=2LH_{kv}ds
$$
其中系数 2 分别对应 Key 和 Value。单个请求长度从 $t$ 增加到 $t+1$ 时,所有层都要追加新的 K/V;大量请求又会在不同时间到达、增长和结束。因此,KV Cache 同时具有 体积大、长度动态、生命周期不一致 三个特征。
Block Table 如何工作
PagedAttention 把一条请求逻辑连续的 KV Cache 划分为 Logical Blocks,再从全局显存池分配 Physical Blocks:
1 | 请求的逻辑顺序: L0 -> L1 -> L2 -> L3 |
Attention Kernel 读取 Block Table,把逻辑块编号转换为物理 Block ID。于是请求看到的 KV Cache 仍按 Token 顺序连续,物理显存却不需要连续。
当序列继续生成时,系统只需在当前 Block 写入新 K/V;Block 填满后再分配一个空闲物理块并更新映射。请求结束后,其不再被引用的 Block 可以返回空闲池,供其他请求复用。
下图把连续预留与分页映射放在同一张图中,重点观察 KV Cache 的物理布局如何逐步影响并发容量和系统吞吐。
图中的关键不是 Block Table 本身节省计算,而是它解除了“逻辑连续必须等于物理连续”的约束。vLLM 因此能够按需分配 KV Block,并把节省下来的显存用于容纳更多活跃请求。
PagedAttention 的优势
降低显存浪费
按需分配避免按最大序列长度预留完整 KV Cache。固定大小的 Block 也消除了“必须找到一整块连续大空间”的要求;内部碎片通常只剩每条序列最后一个未填满的 Block。
原始论文将这种目标概括为 near-zero waste。这是相对传统预留和连续分配的内存管理结论,不表示 KV Cache 本身不再占显存。
提高并发与吞吐
显存能容纳的活跃请求数近似受下面的容量关系限制:
$$
N_{active}\lesssim\frac{M_{GPU}-M_{weights}-M_{runtime}}{\operatorname{avg}(M_{KV/request})}
$$
PagedAttention 减少分母中的无效预留和碎片后,调度器可以保留更多活跃请求,并通过 Continuous Batching 在每轮 Decode 中组成更大的有效 Batch。GPU 利用率和系统吞吐因此提高。
原始 vLLM 论文在其测试工作负载中,相较当时的 FasterTransformer 和 Orca 报告了约 2–4 倍吞吐提升。这个数字来自 PagedAttention、Block Manager、调度与执行 Kernel 的整体协同,不能解释成分页寻址单独让某个 Attention Kernel 快了 2–4 倍。
支持块级共享
相同前缀、Parallel Sampling 和 Beam Search 可能拥有相同的历史 K/V。块式布局允许多个逻辑序列引用同一批 Physical Blocks;只有分支真正产生不同内容时,才通过 Copy-on-Write 分配新块。
自动 Prefix Caching 也可以按完整 KV Block 建立 Hash,在后续请求命中相同前缀时复用已计算的 K/V。分页不是前缀缓存的充分条件,但提供了自然的共享和引用计数粒度。
改善调度与回收
Block 是统一的容量单位,调度器可以围绕空闲块数量进行接纳、抢占、回收和缓存淘汰。相较管理大量不同尺寸的连续分配,容量判断更明确,也不容易出现总空闲显存尚可、却找不到足够大连续区域的问题。
不用 PagedAttention 会怎样
“不用 PagedAttention”至少包含三种不同情况,不能混为一谈。
直接从 vLLM 删除
vLLM 的 KV Cache Manager、Scheduler、Block Table 和 Attention Backend 是配套设计。若直接删除 PagedAttention 而不提供替代实现:
- Scheduler 分配的 Block 无法被 Kernel 正确寻址;
- KV Cache 的追加、回收和复用失去对应布局;
- Prefix Caching、共享和抢占恢复等能力失去底层契约;
- 系统无法正常执行,而不只是性能下降。
因此,PagedAttention 不是一个可以独立关闭的显示选项。要移除它,必须同时替换 KV Cache 分配器、地址映射和兼容的 Attention Kernel。
替换成连续 KV Cache
如果使用正确实现的连续 KV Cache,模型仍能得到相同结果,但高并发服务通常会出现以下问题:
- 提前预留浪费。 按最大长度分配时,短请求占用大量未使用空间。
- 动态扩容困难。 按实际长度增长时,扩容可能需要重分配和数据复制。
- 外部碎片增加。 不同长度请求频繁到达和结束后,空闲空间被切成小块。
- 并发容量下降。 相同显存只能驻留更少请求,Continuous Batching 的 Batch 规模受限。
- 吞吐下降、排队增加。 高负载下,更多请求必须等待 KV 空间,尾延迟更容易恶化。
- 共享成本上升。 相同前缀或生成分支更容易复制整段连续 KV Cache。
- OOM 更难预测。 即使空闲容量总和足够,也可能没有满足要求的连续区域。
不过,PagedAttention 不是动态 KV Cache 管理的唯一可能方案。例如 vAttention 使用 GPU 虚拟内存机制维持连续虚拟地址,并按需映射物理显存,说明“不采用 PagedAttention”不等于“只能低效推理”。
完全关闭 KV Cache
这比不用 PagedAttention 更严重。没有 KV Cache 时,每生成一个 Token,都要重新处理已经计算过的前缀,反复生成历史位置的 K/V。显存中的持久 KV 状态减少了,但重复计算会使 Decode 显著变慢。
两者的职责应明确区分:
| 机制 | 解决的问题 | 缺失后的主要后果 |
|---|---|---|
| KV Cache | 避免重复计算历史 Token 的 K/V | 每轮重新计算前缀,Decode 极慢 |
| PagedAttention | 高效分配、寻址、回收和共享 KV Cache | 显存浪费与碎片增加,并发和吞吐下降 |
适用边界与常见误解
低并发场景收益有限
如果只有一个短请求,序列长度固定且显存充足,连续 KV Cache 已经足够简单高效。此时 PagedAttention 扩大并发容量的优势无从体现,Block Table 间接寻址还会增加实现复杂度。
不改变模型精度
PagedAttention 改变的是 KV Cache 的物理布局和访问路径。只要 Kernel 实现正确,逻辑 Token 顺序和注意力计算保持不变,因此不会因为分页本身产生近似误差。
不等于所有 vLLM 优化
Continuous Batching、Chunked Prefill、Prefix Caching、Speculative Decoding 和 KV Cache Quantization 是不同机制。它们可以建立在分页式 KV 管理之上并相互协作,但不能都归因于 PagedAttention。
不会降低单请求的理论读取量
标准 Decode Attention 仍需要让当前 Query 读取可见历史 K/V。PagedAttention 优化的是这些数据的管理和系统容量,并没有自动把长上下文注意力从稠密计算变为稀疏计算。
类比与术语对应
| 直观说法 | 专业术语 |
|---|---|
| GPU 仓库 | GPU HBM / 显存 |
| 请求不断产生的货物 | 持续增长的 KV Cache |
| 固定大小的格子 | Physical KV Block |
| 请求内部的第几个格子 | Logical Block |
| 记录格子顺序的地址表 | Block Table |
| 最后一个格子未装满 | Internal Fragmentation |
| 空闲空间散落成小洞 | External Fragmentation |
| 多个请求共用已有格子 | Block Sharing / Reference Counting |
| 修改后才复制 | Copy-on-Write |
| 同时服务更多请求 | Larger Active Batch / Higher Concurrency |
自检问题
- PagedAttention 主要节省模型参数显存,还是 KV Cache 的无效占用?为什么?
- 两个请求的最大长度都是 4096,但最终分别使用 100 和 3000 个 Token。按最大长度连续预留与按 Block 分配会有什么差异?
- 只有一个短请求、长度固定且显存充足时,PagedAttention 是否仍必然显著加快推理?为什么?