4、PagedAttention核心思想:借鉴操作系统虚拟内存分页机制,将KV Cache划分为固定大小的块(Block)
在大语言模型推理过程中,KV Cache 的显存占用是制约吞吐量和并发数的核心瓶颈。传统实现中,KV Cache 以连续内存方式为每个请求预留完整空间,导致严重的内部碎片和外部碎片。PagedAttention 的突破性思路,是将操作系统虚拟内存的分页机制引入 GPU 显存管理:把连续的 KV Cache 逻辑空间切分为固定大小的"页"(Block),通过块表(Block Table)建立逻辑页到物理块的映射,从而让显存分配像操作系统管理内存一样灵活、高效。本章将深入拆解这一核心思想的原理、实现路径与工程实践。
4.1 核心原理:从连续内存到分页管理的范式转换
传统 KV Cache 实现中,推理引擎为每个序列预先分配一段连续显存,大小由 max_seq_len × hidden_size × num_layers × 2 × dtype_size 决定。这种"一次性预留"策略存在两个致命问题:其一,实际生成 token 数远小于最大长度时,预留空间大量闲置,造成内部碎片;其二,不同请求的生命周期不同,频繁分配释放导致显存空洞,即外部碎片。以 7B 模型为例,若最大序列长度 2048,单请求 KV Cache 预留约 1GB 显存,但实际平均生成 512 token 时,利用率仅 25%。
PagedAttention 彻底改变了这一格局。它将 KV Cache 的逻辑地址空间划分为固定大小的 Block,每个 Block 可容纳固定数量的 token 对应的 Key 和 Value 张量。物理显存中,Block 可以存放在任意位置,无需连续。每个序列维护一张块表(Block Table),记录其逻辑 Block 到物理 Block 的映射关系。Attention 计算时,GPU kernel 通过块表间接寻址,按块读取 KV 数据参与计算。
4.2 块表结构与寻址机制
块表是 PagedAttention 的核心数据结构。每个序列(Sequence)拥有独立的块表,块表条目数等于该序列当前占用的逻辑 Block 数。每个条目包含两个关键字段:物理块地址(指向 GPU 显存中的实际 Block)和引用计数(用于支持 Copy-on-Write 优化)。
Attention 计算时,给定 token 位置 pos,首先计算逻辑块号 logical_block = pos / block_size,块内偏移 offset = pos % block_size。然后通过块表查询物理块地址 physical_block = block_table[logical_block],最终在物理块的 offset 位置读取 KV 数据。整个过程涉及两次内存访问(块表 + 数据),但 GPU 的 L2 Cache 能有效缓存块表热点数据,性能开销可忽略。
| 参数 | 典型值 | 说明 |
|---|---|---|
| Block Size(token 数) | 16 / 32 | 越小碎片越少,但块表越大;越大块表越小,但内部碎片增加 |
| 块表条目大小 | 8 字节 | 4 字节物理地址 + 4 字节引用计数 |
| 块表总大小 | 序列数 × 最大块数 × 8B | 通常小于 1MB,可常驻 GPU L2 Cache |
| 物理块总数 | 显存容量 / 单块大小 | 例如 80GB 显存,单块 4MB → 约 20000 块 |
4.3 分配与释放流程:按需分配与引用计数
PagedAttention 的显存分配遵循"按需分配、用完即还"原则。具体流程如下:
- 初始化:推理引擎启动时,将可用显存划分为固定大小的物理块池(Block Pool),维护空闲块链表。
- 请求到达:为序列创建空块表,初始不分配任何物理块。
- Prefill 阶段:根据输入 prompt 的 token 数,计算所需块数
ceil(prompt_len / block_size),从空闲链表中取出对应数量的物理块,填入块表。 - Decode 阶段:每生成一个新 token,检查当前最后一个逻辑块是否已满。若已满,则从空闲链表分配一个新物理块,追加到块表末尾。
- 序列结束:释放该序列所有物理块,归还空闲链表,清除块表。
引用计数机制则服务于 Beam Search 和并行采样场景。当多个序列共享同一个前缀时(如 Beam Search 中多个候选序列共享已生成的前缀),无需为每个序列复制前缀的 KV Cache,而是让它们的块表条目指向相同的物理块,并将引用计数加 1。当某个序列不再需要该前缀时,引用计数减 1;仅当计数归零时,物理块才真正释放。这种 Copy-on-Write 策略极大节省了显存。
4.4 案例拆解:多请求并发下的显存节省效果
我们以一个具体场景对比传统方案与 PagedAttention 的显存表现。假设 GPU 为 A100-80GB,模型为 Llama-7B(FP16),单层 KV Cache 大小为 2 × 128 × 8 × 2B = 4KB/token,32 层共 128KB/token。设置最大序列长度 2048,Block Size 为 16。
场景设定:同时处理 64 个请求,每个请求实际生成长度分布为:32 个请求生成 256 token,16 个请求生成 512 token,16 个请求生成 1024 token。所有请求的 prompt 长度均为 128 token。
传统连续分配:每个请求预留 2048 token 的 KV 空间,即 2048 × 128KB = 256MB。64 个请求共需 64 × 256MB = 16GB。实际使用量:32×(128+256) + 16×(128+512) + 16×(128+1024) = 32×384 + 16×640 + 16×1152 = 12288 + 10240 + 18432 = 40960 token,对应 40960 × 128KB = 5GB。显存利用率仅 31.25%。
PagedAttention 分页分配:每个请求按实际 token 数分配 Block。总 token 数 40960,Block Size 16,共需 40960 / 16 = 2560 个 Block。每个 Block 大小 16 × 128KB = 2MB。总显存占用 2560 × 2MB = 5GB。与传统方案相比,节省了 11GB 显存,相当于多容纳 43 个同等规模的请求。
| 指标 | 传统连续分配 | PagedAttention | 提升 |
|---|---|---|---|
| 预留显存 | 16 GB | 5 GB | 节省 68.75% |
| 显存利用率 | 31.25% | ~100% | 3.2 倍 |
| 可并发请求数(80GB) | 约 312 个 | 约 1000 个 | 3.2 倍 |
| 块表额外开销 | 无 | 2560 × 8B ≈ 20KB | 可忽略 |
实际部署中,vLLM 通过 PagedAttention 将 Llama-7B 的推理吞吐量提升了 2-4 倍,核心原因就是显存利用率的大幅提升使得 batch size 可以成倍增加。值得注意的是,块表本身也占用显存,但相比节省的空间,这部分开销微乎其微。
4.5 常见误区与工程复盘
复盘经验:在将 PagedAttention 集成到生产系统时,建议按以下步骤验证:首先,用 profiling 工具统计实际 KV Cache 使用量分布,确定合理的 Block Size;其次,在单卡环境对比传统方案与 PagedAttention 的吞吐量和延迟;最后,在分布式环境(如 Tensor Parallel)中验证块表同步机制的正确性。特别要注意,当使用张量并行时,每个 GPU 持有部分 KV Head,块表需要在各 GPU 上保持一致,这要求块表更新操作具备跨设备同步能力。
PagedAttention 的分页思想不仅解决了显存碎片问题,更开启了 KV Cache 管理的新范式——将显存视为可精细调度的资源池,而非一次性分配的固定空间。这一设计为后续的 Prefix Caching、自动混合精度、显存交换等高级特性奠定了基础,是 vLLM 高性能推理的核心基石。