AI 技术AI 推理 · 16/18
#PagedAttention#vLLM#KV Cache#虚拟内存#Continuous Batching#LLM Serving

AI 推理系列(十六):PagedAttention——为什么 vLLM 能把 GPU 显存“像操作系统一样”管理?

围绕在线大模型服务中动态增长的 KV Cache,解释 PagedAttention 如何通过逻辑块、物理块和块表实现按需分配,并分析内部碎片、Copy-on-Write、Prefix Caching、Continuous Batching 及长上下文场景中的工程边界。

一个在线推理服务可以在同一张 GPU 上加载完全相同的模型,却因为 KV Cache 管理方式不同,表现出截然不同的并发能力。模型权重没有变,Attention 公式没有变,单个请求需要执行的 Transformer 层也没有变;变化发生在运行时:系统能否把有限显存及时分给正在增长的请求,能否在请求结束后立即回收,并让新的请求继续进入执行批次。

PagedAttention 解决的就是这类状态管理问题。它不是一种新的注意力数学形式,而是一套面向 KV Cache 的分页式存储和访问机制。vLLM 借鉴虚拟内存中的逻辑页、物理页和映射表,把一个请求逻辑上连续的 KV 序列放进物理上不连续的显存块,再通过专用 Attention Kernel 按块读取。它带来的价值不只是少浪费一些显存,而是让调度器可以在请求长度未知、批次成员持续变化的条件下可靠地分配和回收缓存。

KV Cache 为什么会把在线服务变成内存管理问题

自回归模型生成新 Token 时,会为这个位置计算每层 Attention 所需的 Key 和 Value。历史位置的 K/V 不会在每一步重新计算,而是留在显存中供后续 Query 使用。一个请求的缓存量因此大致随以下因素共同增长:层数、KV Head 数、Head Dimension、数据类型以及已经处理的 Token 数。

单个请求并不难管理。困难来自大量请求同时存在,而且它们的 Prompt 长度、生成长度和结束时间都不同。请求 A 可能生成几十个 Token 后结束,请求 B 可能持续数千 Token,请求 C 还在 Prefill,请求 D 已经进入 Decode。调度器在每个迭代周期都要处理缓存增长、请求完成、超时取消、序列分叉和新请求进入。

如果系统为每个请求按最大长度预留一块连续缓存,会产生明显的内部浪费。一个只使用 300 个位置的请求若预留了 4096 个位置,对应的大部分空间从未写入,却不能分给其他请求。如果系统改成按当前长度反复申请不同大小的连续区域,又容易在请求结束后留下大小不一的空洞。此时总空闲量可能足够,但无法找到满足某个大请求的连续区域,这就是外部碎片。

PagedAttention 论文在其评估和基线分析中指出,传统 KV Cache 管理可能因预留、内部碎片和外部碎片浪费大量显存,并给出过 60%~80% 的典型浪费观察。这个数字不是所有框架、模型和流量下的固定比例,但说明一个事实:在线服务能放入多少并发请求,常常先由缓存分配方式决定,而不是由理论 FLOPs 决定。

PagedAttention 如何把连续序列映射到离散显存块

PagedAttention 将 KV Cache 划分为固定容量的 Block。一个逻辑 Block 对应连续的一小段 Token 位置,一个物理 Block 则是 GPU Cache Pool 中实际存储这些 K/V 的空间。请求维护一张 Block Table,用来记录“第几个逻辑 Block”当前对应“哪个物理 Block”。

假设一个请求已经占用三个逻辑 Block,它在语义上仍是一条连续序列:

逻辑序列:Block 0 → Block 1 → Block 2

实际显存位置却可以是:

物理位置:Block 17、Block 4、Block 29

只要 Block Table 保存映射关系,Attention Kernel 就能先确定目标 Token 位于哪个逻辑块,再读取对应的物理块。请求看到的是连续上下文,分配器操作的则是一组可独立回收的等尺寸块。

flowchart LR
  A[请求 Token 序列] --> B[切分逻辑 Block]
  B --> C[查询 Block Table]
  C --> D[映射物理 Block]
  D --> E[读取分散 KV]
  E --> F[执行 Attention]
  F --> G[生成新 Token]
  G --> H{当前 Block 已满}
  H -->|否| I[追加 KV]
  H -->|是| J[申请新物理 Block]
  J --> C
  I --> C

这种设计不会把所有浪费降为绝对零。每个请求最后一个 Block 仍可能没有填满,因此还存在由 Block 粒度决定的内部碎片;Block Table、引用计数和分配器元数据也需要空间。它真正做到的是把浪费限制在较小、可预测的边界内,并消除“必须为整个请求寻找一块同等长度连续物理显存”的要求。

Block Size 是显存利用率与访问效率之间的权衡

Block 越小,最后一块未填满造成的浪费通常越少,请求增长时也能更细粒度地申请显存。但小 Block 会让一个长请求拥有更多表项,Attention Kernel 需要处理更多块边界,分配器和调度器也会进行更频繁的元数据操作。

Block 越大,Block Table 更短,单次访问更容易形成较大的连续工作单元,但短请求或大量刚开始生成的请求可能在最后一块留下更多未使用位置。最佳 Block Size 因模型结构、KV 数据类型、Prompt 分布、输出长度和 Kernel 实现而异,不能只按“越小越省显存”选择。

Block 选择主要收益主要成本更可能适合的负载
较小 Block最后一块浪费较少,分配粒度细表项更多,边界与调度开销增加大量短请求、长度离散度高
较大 Block元数据较少,访问和调度更规整尾块浪费可能增加长请求占比高、Kernel 偏好大块
固定连续缓存地址计算简单,Kernel 容易实现预留和碎片风险高,难适应动态请求长度可预测的封闭批处理

生产调优应基于真实长度直方图,而不是只用平均长度。两个服务即使平均 Prompt 都是 2000 Token,只要一个集中在 1800~2200,另一个同时存在 100 与 10000 Token 请求,对 Block 粒度和调度策略的需求就可能完全不同。

Copy-on-Write、并行采样与 Prefix Caching 不是同一件事

分页映射让多个逻辑序列可以指向同一个物理 Block。这个能力对并行采样和 Beam Search 很有价值:多条候选在分叉前共享相同前缀,系统只保存一份前缀 KV;当某条候选需要写入共享尾块时,再为它复制或分配独立 Block。这与操作系统的 Copy-on-Write 思路相似。

但“能够共享物理块”不等于“自动拥有跨请求 Prefix Caching”。Copy-on-Write 描述的是多个已知相关序列如何安全共享和分叉;Prefix Caching 还需要解决缓存键、Token 序列匹配、模型与位置配置一致性、缓存生命周期、淘汰策略以及集群路由。PagedAttention 提供了适合共享的块级表示,Prefix Caching 则是在此基础上决定哪些历史 Block 可以被未来请求命中。

这一边界很重要。系统 Prompt 文本看起来相同,但 Tokenizer、Chat Template、特殊 Token、工具定义顺序或模型版本发生变化时,对应 KV 语义可能已经不同。可靠的前缀缓存不能仅因为字符串相似就共享物理 Block。反过来,即使 Prefix Cache 已经命中,剩余未命中的后缀仍要正常 Prefill,后续生成也要继续分配新的 KV Block。

Continuous Batching 为什么依赖可快速回收的 KV Cache

Continuous Batching 解决的是批次生命周期,而 PagedAttention 解决的是缓存分配。两者经常一起出现,是因为在线服务中的批次成员会在每轮迭代后变化:完成的请求退出,等待队列中的请求进入,仍在生成的请求继续占用已有状态。

静态批处理通常等整批请求全部完成后才释放资源。若一个请求生成 20 个 Token,另一个生成 500 个 Token,前者对应的执行槽位可能长时间空置。Continuous Batching 允许调度器在迭代边界移除已经完成的序列,并补入新请求,从而减少这种批次气泡。

这要求缓存管理器能够同步完成三件事:回收结束请求的物理 Block,为新请求分配 Prompt/Decode 所需 Block,并保证仍在运行的请求 Block Table 不受影响。若每次成员变化都需要移动大块连续 KV 或执行昂贵的内存整理,Continuous Batching 的调度收益会被数据搬运抵消。分页式缓存让调度器可以主要操作 Block 所有权和映射,而不是重排整个显存布局。

机制解决的问题不直接解决的问题
PagedAttentionKV Cache 的分配、映射、回收与块级访问不减少模型生成所需算术量
Continuous Batching动态请求流中的批次空洞与调度利用率不自动消除 KV 碎片
Prefix Caching复用已经计算过的相同前缀 KV不降低新后缀和 Decode 成本
GQA / MLA从模型结构上减少或压缩 KV 状态不负责物理缓存生命周期
KV Cache Quantization降低每个缓存元素的存储位数可能引入量化误差和转换成本
FlashAttention优化 Attention Kernel 的分块与数据移动不等同于服务级缓存分配器

非连续 KV 为什么需要专门的 Attention Kernel

普通 Attention 实现往往假设 K/V 在地址空间中按序列维度连续排列。PagedAttention 打破了这个假设:下一个逻辑 Token 可能位于另一个物理 Block,Kernel 必须读取 Block Table,计算块内偏移,再访问正确的 K/V。

这会带来额外地址计算和间接访问。如果实现只是用通用算子逐块拼接 K/V,再执行普通 Attention,可能产生额外复制和 Kernel Launch,抵消显存管理收益。vLLM 因此需要针对分页布局设计执行路径,让线程按 Block 读取数据、尽量形成合并访存,并在计算过程中处理逻辑位置与物理地址的转换。

PagedAttention 的端到端收益因此来自系统协同,而不是单个数据结构。Block Allocator 降低浪费,Scheduler 提高活跃批次利用率,Paged Attention Kernel 避免把分散块重新拼成连续缓存。任何一层实现不匹配,都可能让系统出现“显存看起来省了,但每 Token 延迟变差”的结果。

vAttention 为什么重新考虑连续虚拟地址

PagedAttention 借鉴分页思想,但通常由推理引擎和专用 Kernel 显式维护块映射。vAttention 提出了另一种路线:利用 CUDA 虚拟内存能力,为每个请求保留连续的虚拟地址范围,再按需把物理显存页映射进去。这样,Kernel 可以继续看到连续虚拟地址,而物理显存仍按页分配。

它试图减少显式 PagedAttention Kernel 带来的地址映射和适配复杂度,同时保留按需提交物理内存的能力。论文在其模型、硬件和负载配置下报告了相对分页式基线的吞吐改善,但最高约 1.23× 的结果不能直接外推到所有 vLLM 部署。驱动与运行时能力、页映射开销、模型形状、并发度和现有 Kernel 优化程度都会改变结果。

PagedAttention 与 vAttention 的差别不是“分页”和“不分页”这么简单。两者都区分逻辑地址与物理存储;关键差异在于映射由谁维护、Kernel 看到什么地址布局,以及动态映射操作能否被低成本地放进请求生命周期。后续将分页缓存与 FlexAttention 等更灵活 Kernel 结合的工作,也是在继续优化这条边界。

如何判断服务瓶颈是否真的来自 PagedAttention

PagedAttention 适合解决动态 KV 分配问题,但不能把所有吞吐下降都归因于缓存碎片。若 Prompt 很长,TTFT 主要耗在 Prefill 计算,应该同时检查 Attention Kernel、Chunked Prefill 和 Prompt 缓存命中;若 Decode TPOT 随上下文增长明显恶化,瓶颈可能在 KV 读取带宽;若 GPU Cache 使用率很低但等待队列持续增长,则可能是计算、跨卡通信或调度预算受限。

生产环境至少应拆分观察以下信号:

  • KV Cache 物理 Block 使用率、空闲 Block 数量与分配失败次数;
  • 每个请求占用 Block 数量以及最后一块平均填充率;
  • 运行、等待和被抢占请求数量;
  • Prompt 与生成长度分布,而不只是平均值;
  • TTFT、TPOT/ITL、P95/P99 延迟和 Goodput;
  • Prefix Cache 的命中 Token 数,而不只是请求命中次数;
  • Attention Kernel 耗时、Block Table 访问与 HBM 带宽;
  • CPU Offload 或 Swap 发生频率及其延迟代价。

如果 Cache Pool 经常接近耗尽,PagedAttention 只能让剩余空间使用得更充分,不能创造额外容量。此时应评估降低并发、限制最大上下文、使用 GQA/MLA、量化 KV、增加设备或采用 Prefill-Decode 分离。若问题是 Kernel 处理分页布局的效率低,则增加更多 Block 反而可能加重开销,需要回到真实执行路径做 Profiling。

哪些场景不会因为分页而自动变快

第一类是 KV Cache 本身不是主要约束的短请求、低并发服务。请求数量很少且长度稳定时,连续分配简单直接,分页带来的元数据和间接访问未必产生可见收益。

第二类是极长上下文。PagedAttention 改善的是“怎么放”,不会减少每个 Token 对应的 KV 元素数量。一个 128K 或更长请求即使没有外部碎片,仍可能独占大量物理 Block;当总 Cache Pool 被真实数据填满后,分页无法继续提升容量。

第三类是计算或通信受限的部署。模型权重读取、Tensor Parallel All-Reduce、MoE All-to-All、采样或 Tokenizer 都可能成为瓶颈。此时显存利用率提升并不必然转化成更高吞吐,因为新增请求只会进入另一个受限环节。

第四类是请求优先级和延迟 SLO 冲突。Continuous Batching 能维持 GPU 利用率,但调度器仍要决定长 Prompt、短 Decode、低优先级批任务和实时请求如何共享 Token Budget。PagedAttention 让这些请求可以共存,却不负责定义公平性或业务优先级。

PagedAttention 的工程意义正在于这一边界:它把 KV Cache 从“每个请求私有的一整块连续数组”变成可分配、可映射、可共享和可回收的运行时资源。模型数学没有变化,但推理服务获得了类似操作系统内存管理器的能力。对高并发、长度差异显著的在线负载,这种状态管理方式能够释放原本被预留和碎片占据的显存,并为 Continuous Batching、Prefix Caching 和复杂解码策略提供可组合的基础;对真实 KV 容量、计算、带宽和调度问题,它仍需要与其他模型和系统优化共同工作。

资料来源

  1. Efficient Memory Management for Large Language Model Serving with PagedAttention
  2. Efficient Memory Management for Large Language Model Serving with PagedAttention
  3. vLLM Paged Attention Documentation
  4. vLLM Documentation
  5. TensorRT-LLM Batch Manager
  6. vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention
  7. Paged Attention Meets FlexAttention: Unlocking Long-Context Efficiency in Deployed Inference