分页注意力 PagedAttention

PagedAttention (Page-based KV Cache Management)

受操作系统虚拟内存分页思想启发,将大模型推理过程中的 KV 缓存按固定大小的页块切分、离散存储并按需分配与回收,消除连续显存碎片,使 KV 显存利用率从 40% 提升至 85% 以上,是 vLLM、SGLang 等框架支持高并发连续批处理的基础内存管理机制

详细解释

分页注意力 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 OffloadvLLM、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+ 多租户网关

实战经验与避坑

  1. 页块大小 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。
  2. 开 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% 并发。
  3. 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,老的长请求延迟容忍度更高可以慢一点换回。
  4. 超长上下文(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 路,不至于掉到个位数。
  5. 多租户部署时要打开「Per-Request Page Budget(单请求页配额)」,防止单条长请求吃掉 80% 显存饿死其他租户:分页注意力很好,但它默认是「全局公平」——谁先到谁占页,一条长请求生成万把字会把空闲页池吃光,新进来的 100 条短请求全拿不到 KV 页触发 OOM 或排队等待。正确姿势:在多租户网关层或推理引擎本身设置单请求的 Page Budget 上限(例如单请求最多占 4K/8K Token 对应的页数,超过就触发 Sliding Window 淘汰或降级 Offload)。这样一条长请求最多用掉 10% 至 20% 的 KV 显存,其余 80% 永远留给其他租户的短请求,SLA 稳定性会大幅提升。
  6. PagedAttention 是推理引擎的功能,不要试图在应用层自己手写一套页管理:不少团队为了「更可控」,在 FastAPI 应用层自己写 KV 页分配、回收、换页逻辑,结果花了几个月写出来的版本性能还不如 vLLM 默认配置。原因是 PagedAttention 的收益依赖于「注意力 CUDA Kernel 本身能以页为单位读取非连续的 KV 并做高效拼接」——这件事必须在 CUDA/C++ 层的算子里做,Python 层写逻辑可以调度,但做不到算子级的连续访存优化,性能至少差 1.5 至 3 倍。正确做法:直接用 vLLM / SGLang / TensorRT-LLM 原生的 PagedAttention 算子,应用层只做并发控制、配额、Swap 阈值,不要重复造 CUDA 轮子。

常见问题

分页注意力和前缀缓存有什么关系和区别?为什么 vLLM 两个开关都要开?
两者是正交的两层优化——一个解决「KV 怎么放」,一个解决「重复的 Prefill 还要不要重算」,叠加是 1+1 大于 2 的关系。先把边界划清楚:分页注意力 PagedAttention = 内存管理层优化,核心任务是「KV 显存按页切、不要求物理连续、页表映射、请求结束立即回收页」,目标是消灭碎片、提升 KV 显存利用率;它在 Prefill 阶段和 Decode 阶段都生效,但并不关心「这些 KV 的数值是不是之前算过一模一样的」。前缀缓存 Prefix Caching = 计算层优化,核心任务是「当两条请求有完全相同的前缀(比如相同的 System Prompt + 相同的 RAG 检索段落 + 相同的几轮历史),Prefill 阶段对这些前缀 Token 做过的 Attention 计算就直接跳过,从缓存里把已经算好的 KV 拿过来用」,目标是降低 Prefill 时延和算力浪费。两者为什么必须叠加?——举个例子:100 条请求都共享同一个 2000 Token 的 System Prompt。只开 PagedAttention:每条请求的前 2000 个 KV Token 各自占 2000 除以 16 等于 125 页,但物理上是 100 份完全相同的页内容,显存里重复 100 次;Prefill 阶段也会为这 2000 Token × 100 条重复跑 100 次 Attention,算力浪费严重。只开 Prefix Caching 不开 PagedAttention:计算确实省了 Prefill 算力,但 KV 还是连续分配——100 条请求各自一块连续 KV,会产生巨大碎片,KV 显存利用率又回到 40% 以下,并发上不去。两者都开:① Prefill 阶段 Prefix Caching 命中前缀 → 100 条请求的前 2000 Token 只 Prefill 1 次,Prefill 总时长下降 80% 至 95%;② 内存层 PagedAttention 的 Share-Prefix 把这 2000 Token 对应的 KV 页只保留 1 份物理副本,其他 99 条请求通过页表指针指向同一批物理页,KV 显存又省下 99% 的前缀部分。两者叠加后 RAG/Agent 类场景(大量共享前缀)整体收益最大——典型实测:相同 P99 延迟下单卡并发 3 倍以上、成本下降 70% 以上。唯元智创 默认两个开关都开,并在网关层按租户和 App ID 聚合前缀哈希,进一步提升 Prefix Cache 的命中率,让企业多租户部署获得最佳性价比。
分页注意力会不会引入额外的注意力计算开销?Page Size 怎么选才最优?
会有极小的元数据查找开销,但和它带来的显存利用率 + 并发提升收益相比完全可以忽略;Page Size 绝大多数场景用引擎默认值即可,极端场景再微调。先回答开销问题:PagedAttention 在注意力计算前需要先通过「请求 ID → 页表 → 物理页地址」拿到 KV 的物理地址,再把多页非连续的 KV 拼起来做 FlashAttention。这个查找过程确实比「直接拿一块连续 KV 指针」多了页表索引,但 CUDA 实现里会把页表在 Kernel Launch 前就打包到连续的元数据数组里,并和 FlashAttention 的 Tile 调度融合在一起,实测引入的额外算力开销仅 1% 至 3%,而带来的并发提升通常是 2 至 4 倍,净收益巨大。Page Size 的选择可以按三条经验法则:第一条,默认引擎值优先——vLLM/SGLang 默认 Block Size 16,TensorRT-LLM 默认 32,不要先动。这两个值是在长短混合真实业务压测下得到的黄金点,兼顾了外部碎片(页越大越容易浪费半页内部碎片)和页表元数据开销(页越小页表越大、查找次数越多)。第二条,按你的业务请求平均输出长度反推:如果你的应用 95% 以上请求输出都在 32 Token 以内(如分类、抽取、短回复),就把 Page Size 调到 8,这样内部碎片(请求输出只占 32 Token 里 10 个那浪费的 22 个)会少很多;反过来如果 95% 以上都是长输出(512 至 4096 Token),就调到 32,页表更小、Kernel 调度更高效。第三条,一定要 A 或 B 实测,不要纸上谈兵:在你的真实流量分布下做 30 分钟等比压测,分别把 Page Size 设为 8、16、32 跑三组,记录「GPU 利用率、KV 显存占用、P99 延迟、TPS」四个指标,挑综合最优的组合。经验结论:98% 的企业业务,默认 Page Size 16 都能拿到最优或接近最优的综合表现,调到 8 或 32 的边际收益都在正负 5% 以内,除非你业务负载极其单一才值得改。
在量化(AWQ/GPTQ/FP8)场景下,分页注意力还能正常工作吗?KV Cache 的数据类型怎么选?
完全兼容,PagedAttention 在 AWQ、GPTQ、FP8、INT8 KV Cache 量化场景下都能正常工作,甚至因为 KV 变小,每页能放更多 Token,KV 利用率还能再涨一截。两者没有根本冲突——PagedAttention 只关心「KV 的内存布局是按页切」,不关心 KV 每个元素本身是 16-bit FP8 还是 8-bit INT;量化只是改变每个 KV 元素的字节数,页大小按「每页多少 Token」定义就能自动适配不同字节宽度。比如 FP16 KV 每个 Token 的 K+V 对 70B 模型 = 70B × 2 × hidden_size 对应的字节数约 1400 字节,FP8 就砍一半约 700 字节,INT8 再略小。KVCache 量化的推荐选择分三个档位:第一档,质量最优默认:KV Cache dtype = FP8(Hopper/Ada 架构起)或 FP16(老 Ampere),精度损失几乎无法感知,KV 显存占比降一半,PagedAttention 开了后 KV 利用率又翻倍,合计 4 倍的 KV 容量提升,是绝大多数生产部署首选。第二档,极致并发场景:KV Cache dtype = INT8 + per-channel symmetric 量化,精度损失会在部分超长数学推理、代码生成任务上有 1% 至 3% 的下降,但 KV 再省 1 倍,同显存并发再涨 30% 至 50%。适合 C 端免费产品、资讯类生成等对轻微质量退化不敏感的场景。第三档,不要碰:INT4 KV Cache,目前算子支持度低、精度掉得很明显(长上下文注意力偏航、事实错乱率上升 10% 以上),除了极少数移动端或边缘端 8GB 显存以下的场景,生产环境不推荐。一个常见坑:权重是 AWQ/GPTQ INT4 量化,KV Cache 也被迫跟着 INT4,这是完全没必要的——权重量化和 KV Cache 量化是两个独立开关,可以 AWQ 权重降显存 + FP8 KV Cache 保质量,不要被网上「全栈 4-bit」这种说法绑死,按你的业务质量目标分开选权重 dtype 和 KV dtype 才是最优。唯元智创 的默认旗舰配置就是「AWQ/GPTQ 4-bit 权重 + FP8 KV Cache + PagedAttention + Share-Prefix」四件套,70B 模型单 A100 即可稳定跑 128 至 256 并发,质量和原版 FP16 在绝大多数下游任务上差异小于 0.5%,完全满足企业生产要求。