详细解释
分页注意力 PagedAttention 是 vLLM 团队(2023 年,UC Berkeley)受操作系统「虚拟内存 + 页表」思想启发而提出的 KV 缓存管理方案,也是目前现代推理引擎在显存层最核心、收益最高的创新之一。它把每条请求在自回归解码过程中产生的 Key/Value 向量,不再按「一整条请求一块连续显存空间」的方式分配,而是切成固定大小的「页块」(Block / Page),比如每页存放 16 至 32 个 Token 的 KV。所有空闲页统一管理在「空闲块池」里,请求需要多少就分配多少页、不要求连续;当请求结束后,它占用的页立即拆回空闲池,供后续任何新请求复用。
分页注意力诞生之前,传统 KV 缓存管理面临两个灾难性问题:(1) 显存碎片:请求长短不一、出队时间分散,连续分配的 KV 显存会被掏空成大小不一的空洞,这些空洞未必能塞下新请求的 KV 长度,大量显存被浪费,实际 KV 利用率普遍只有 30% 至 40%;(2) 无法优雅做连续批处理:连续批要求「请求随时进、随时出」,但老的连续 KV 分配方式在出队时会留下空洞,导致连续批的并发上限(max_num_seqs)根本达不到理想状态。分页注意力彻底解决了这两个问题,使 KV 显存利用率从 40% 左右跃升到 85% 至 95%,单卡支持的并发对话数提升 2 至 4 倍,和 连续批处理 一起成为大模型在线推理成本下降 60% 至 80% 的两大基石。
唯元智创 的推理网关默认在 vLLM / SGLang 后端启用 PagedAttention,并开启 Share-Prefix(共享前缀页复用)+ Swap(CPU 内存换入换出)+ CPU Offload(超长请求 KV 放到主机内存) 三个扩展功能:Share-Prefix 能让同一个对话会话里的多条消息、或同一个 RAG 应用相同的 System Prompt,只在显存里存一份公共页,被所有相关请求共享,进一步节省 15% 至 30% 的 KV 显存;Swap + CPU Offload 则在短突发高峰时把长请求不常用的冷 KV 页临时挪到 CPU 内存,短时再涨 30% 至 50% 的并发上限。三者叠加后,70B 模型单 A100 的最大并发从 64 路涨到 192 至 256 路,且 P99 延迟 仍控制在 5 秒以内,极大降低了企业部署的 GPU 投入。
四种 KV 缓存管理策略对比表
| 管理策略 | 核心思想 | KV 显存利用率 | 典型 70B 单 A100 最大并发路数 | 连续批处理适配性 | 支持前缀共享 / 换入换出 | 推理引擎原生支持度 |
|---|---|---|---|---|---|---|
| 连续分配(老方案 Static Contiguous) | 每条请求一段连续显存块,按最大长度预分配 | 30% 至 40% | 16 至 32 | 极差(碎片无法复用) | 不支持 / 不支持 | Hugging Face Transformers 原生、老版 TGI |
| 分桶式 Bucketing | 按长度分桶(如 512/1024/2048 桶),同桶请求复用 | 45% 至 60% | 32 至 64 | 中(同桶可复用) | 部分支持 / 不支持 | TensorRT-LLM 老版本 |
| 分页注意力 PagedAttention | 固定大小页块 + 页表映射,按需分配回收 | 85% 至 95% | 128 至 256 | 极佳(页粒度随时进出) | 原生支持共享前缀页 / 原生 Swap + CPU Offload | vLLM、SGLang、LMDeploy 0.2+、TensorRT-LLM 0.8+ |
| 分页注意力 + KV 缓存淘汰 滑动窗口 | 分页基础上主动淘汰最远 KV 页保存活上限 | 90% 至 98% | 192 至 384(超长上下文) | 极佳 | 支持 / 支持 | vLLM 0.4+、SGLang 0.2+(需配 sliding_window) |
| 唯元智创 默认组合:PagedAttention + Share-Prefix + Swap | 分页 + 前缀共享页 + CPU 内存二级池 | 92% 至 98% | 192 至 320 | 极佳 | 全面支持 / 双级换入换出 | vLLM 0.5+ 多租户网关 |
实战经验与避坑
- 页块大小 Block Size 有最优区间,不要自己随便改默认值:分页注意力的页块(Block Size = 每页多少个 Token 的 KV)是影响最终 KV 利用率 + 注意力计算效率的关键参数。绝大多数推理引擎默认 Block Size = 16,部分支持 32。这是工程上长期验证过的 Pareto 点:如果设得太小(如 4 或 8),页表元数据开销会暴涨,单条请求要查大量页,注意力 kernel 的调度开销变大;如果设得太大(如 64 或 128),每条请求最后一个页里大概率有一半是空的,内部碎片率上升,抵消了分页的收益。实践中 80% 以上的场景直接用默认 Block Size = 16,不要动;只有在你 95% 以上的请求都很短(平均输出 ≤ 64 Token)时才考虑切到 8,或 95% 都是超长输出时切到 32。
- 开 PagedAttention 时务必同时开启 Share-Prefix 前缀共享,不要浪费最昂贵的「重复 System Prompt + RAG 段」:很多团队部署 vLLM 时只开了
--enable-prefix-caching但忘了 PagedAttention 本身的 Share-Prefix 页共享机制(两者是不同的优化,前者是 前缀缓存 在 Prefill 阶段算过的 Query/Key/Value 整体直接跳过重算,后者是在 KV 内存层面直接共享公共前缀的 KV 页指针,两条请求共用同一批物理页)。在 RAG 或 Agent 场景,同一租户下 80% 的请求都共享相同的 System Prompt + 检索文档前缀,Share-Prefix 开启后这些前缀在显存里只保留 1 份,节省 20% 至 50% KV 显存,相当于免费再涨 30% 至 100% 并发。 - Swap 到 CPU 内存不是万能的,一定要设 Swap 阈值和回写优先级:很多人把 vLLM 的
--cpu-offload-gb拉到 200GB,以为「KV 永远不会被挤掉」,结果高峰期出现 P99 延迟从 3 秒涨到 20 秒以上。根因是 Swap 过度——请求缺页时从 CPU 内存把 KV 页 PCIe 搬回 GPU,一次 2MB 的页搬运就需要 0.3 至 1 毫秒,高峰期几百个缺页同时发生就是几十毫秒级延迟。正确做法:① Swap 只做「短时突发缓冲」,CPU Offload 内存限制在 GPU 显存的 1 至 2 倍(如 80GB A100 配 80GB 至 160GB CPU Offload,不要无限大);② 优先 Swap「已连续 ≥ 512 步没有被访问过的冷页」,热页(最近 64 步内访问过的)必须留在 GPU;③ 回写时 Prefill 阶段新分配的页优先级最高,先保证新请求 TTFT,老的长请求延迟容忍度更高可以慢一点换回。 - 超长上下文(128K 以上)+ 高并发时务必组合 Sliding Window KV Cache Eviction,不要光靠 PagedAttention 硬扛:PagedAttention 解决了碎片,但解决不了「单条请求 128K 上下文本身就吃掉几十 GB KV」的问题——一条 128K × 70B × 2(KV)就要吃约 70GB 显存,单 A100 80GB 只能扛 1 条,并发不可能上去。必须叠加 KV 缓存淘汰 的 Sliding Window 策略:比如设窗口 = 8K 或 32K Token,注意力计算只看最近 N 个 Token 的 KV 页,更老的 KV 页要么主动丢弃(Rolling Buffer)、要么先 Swap 到 CPU、要么用 RAG 召回。叠加后超长上下文场景并发数仍能维持在 64 至 128 路,不至于掉到个位数。
- 多租户部署时要打开「Per-Request Page Budget(单请求页配额)」,防止单条长请求吃掉 80% 显存饿死其他租户:分页注意力很好,但它默认是「全局公平」——谁先到谁占页,一条长请求生成万把字会把空闲页池吃光,新进来的 100 条短请求全拿不到 KV 页触发 OOM 或排队等待。正确姿势:在多租户网关层或推理引擎本身设置单请求的 Page Budget 上限(例如单请求最多占 4K/8K Token 对应的页数,超过就触发 Sliding Window 淘汰或降级 Offload)。这样一条长请求最多用掉 10% 至 20% 的 KV 显存,其余 80% 永远留给其他租户的短请求,SLA 稳定性会大幅提升。
- PagedAttention 是推理引擎的功能,不要试图在应用层自己手写一套页管理:不少团队为了「更可控」,在 FastAPI 应用层自己写 KV 页分配、回收、换页逻辑,结果花了几个月写出来的版本性能还不如 vLLM 默认配置。原因是 PagedAttention 的收益依赖于「注意力 CUDA Kernel 本身能以页为单位读取非连续的 KV 并做高效拼接」——这件事必须在 CUDA/C++ 层的算子里做,Python 层写逻辑可以调度,但做不到算子级的连续访存优化,性能至少差 1.5 至 3 倍。正确做法:直接用 vLLM / SGLang / TensorRT-LLM 原生的 PagedAttention 算子,应用层只做并发控制、配额、Swap 阈值,不要重复造 CUDA 轮子。