解码阶段每生成一个 Token 都要把 KV Cache 的增长一段。序列越长,KV Cache 越大。当 Batch=8、序列长度 4096、LLaMA-13B 时——KV Cache 约 22GB。显存不够用了。

PagedAttention 借鉴操作系统的分页内存管理——把 KV Cache 切成固定大小的 Block(页),物理上不连续但逻辑上连续。不同 request 的 Cache 碎片可以被打包到同一物理页上,显存利用率从 40% 提升到 95%。


为什么 KV Cache 会爆显存

KV Cache 在解码阶段的每次迭代中都会增长——当前 Token 的 K/V 追加到缓存末尾。Batch 给固定大小时 Cache 总量需要预分配到最大值。

LLaMA-13B:40 层、40 头、隐藏维度 5120。单 request 在 n=4096 的 KV Cache:

  • 每层 K:[40, 4096, 128] × 2 bytes = 40MB
  • 每层 V:[40, 4096, 128] × 2 bytes = 40MB
  • 40 层:40 × 80MB = 3.2GB

Batch=8 时 Cache = 25.6GB。模型参数 ~26GB。总计 ~52GB——远超单张卡 32GB 的显存。


PagedAttention 为什么出现

标准 Attention 的 KV Cache 是连续分配的——在 Prefill 阶段一次性分配 max_length 的空间。如果实际输出只有 100 个 Token,分配了 4096 容量的空间浪费了 97%。

PagedAttention 的思路是按需分配——每次只分配一个 Block(如 16 或 32 个 Token 的容量),用完再分配下一个。Block 在物理上不连续,通过页表映射到逻辑上的连续地址空间。

物理显存:
Block 0 (page 3) | Block 1 (page 7) | Block 2 (page 1) | Block 3 (page 8)

页表(Block ID → Physical Page ID):
Block 0 → Page 3
Block 1 → Page 7
Block 2 → Page 1
Block 3 → Page 8

逻辑视角:
[Block 0] → [Block 1] → [Block 2] → [Block 3](连续)

Runtime 如何管理 KV Block

CANN Runtime 在 8.0 后原生支持 Paged KV Cache。管理流程:

  1. 初始化。 在模型加载时分配一个 KV Block Pool——从显存中划出一片区域,切成固定大小的 Block
  2. 分配。 解码阶段每步需要新的 Cache 空间时,从 Pool 中取一个空闲 Block
  3. 页表更新。 把新 Block 的物理地址加入页表,更新逻辑到物理的映射
  4. 释放。 推理结束后 Block 返回 Pool
# Paged KV Cache 初始化
cache_config = {
    "block_size": 16,        # 每个 Block 16 个 Token
    "max_blocks": 100000,    # Pool 总 Block 数
    "enable_paged": True,    # 开启分页
}

Block 的大小可以配置。Block 越大,页表项越少(查询开销低),但内部碎片浪费更多。实测中 block_size=16 或 32 是平衡点。


大模型长文本推理场景

PagedAttention 在长文本推理中的显存优势:

场景 标准 Cache Paged Cache 节省
短对话 (n=128) 3.2GB 0.1GB -97%
中等长度 (n=1024) 3.2GB 0.8GB -75%
长文本 (n=4096) 3.2GB 3.2GB 0%
8×批处理 (n=1024) 25.6GB 6.4GB -75%

短序列和批处理场景的节省最明显。8 批短对话的 Cache 从 25.6GB 降到 6.4GB——一张卡可以装下更多并发请求,服务吞吐翻倍。

Paged KV Cache 的页表管理

Paged KV Cache 的页表存储在 NPU 显存中。每张卡的页表大小 = 页表项数 × 页表项大小。

Block 大小=16 Token 时,max_length=4096 的序列需要 256 个 Block。页表项 = 256 项。每项 8 字节——页表总大小 2KB。40 层 × 40 头 × 2(K+V)= 3200 个页表——总页表大小约 6.4MB。相比于 KV Cache 的几 GB,页表开销完全可忽略。

Runtime 对页表的访问是硬件加速的——Paged Attention 的 DMA 从页表查找物理地址只占 Attention 计算时间的 0.5% 以下。

Block 大小的选择

Block 大小的选择影响 Paged KV Cache 的效率:

Block 大小 页表项数 内部碎片 显存利用率
8 512 4% 96%
16 256 6% 94%
32 128 10% 90%
64 64 18% 82%

Block 越小显存利用率越高(碎片少),但页表项数多。CANN 8.0 的默认 Block 大小是 16——在碎片率和页表大小之间取了平衡。

PagedAttention:KV Cache 的分页革命

解码阶段每生成一个 Token 都要把 KV Cache 的增长一段。序列越长,KV Cache 越大。当 Batch=8、序列长度 4096、LLaMA-13B 时——KV Cache 约 22GB。显存不够用了。

PagedAttention 借鉴操作系统的分页内存管理——把 KV Cache 切成固定大小的 Block(页),物理上不连续但逻辑上连续。不同 request 的 Cache 碎片可以被打包到同一物理页上,显存利用率从 40% 提升到 95%。


为什么 KV Cache 会爆显存

KV Cache 在解码阶段的每次迭代中都会增长——当前 Token 的 K/V 追加到缓存末尾。Batch 给固定大小时 Cache 总量需要预分配到最大值。

LLaMA-13B:40 层、40 头、隐藏维度 5120。单 request 在 n=4096 的 KV Cache:

  • 每层 K:[40, 4096, 128] × 2 bytes = 40MB
  • 每层 V:[40, 4096, 128] × 2 bytes = 40MB
  • 40 层:40 × 80MB = 3.2GB

Batch=8 时 Cache = 25.6GB。模型参数 ~26GB。总计 ~52GB——远超单张卡 32GB 的显存。


PagedAttention 为什么出现

标准 Attention 的 KV Cache 是连续分配的——在 Prefill 阶段一次性分配 max_length 的空间。如果实际输出只有 100 个 Token,分配了 4096 容量的空间浪费了 97%。

PagedAttention 的思路是按需分配——每次只分配一个 Block(如 16 或 32 个 Token 的容量),用完再分配下一个。Block 在物理上不连续,通过页表映射到逻辑上的连续地址空间。

物理显存:
Block 0 (page 3) | Block 1 (page 7) | Block 2 (page 1) | Block 3 (page 8)

页表(Block ID → Physical Page ID):
Block 0 → Page 3
Block 1 → Page 7
Block 2 → Page 1
Block 3 → Page 8

逻辑视角:
[Block 0] → [Block 1] → [Block 2] → [Block 3](连续)

Runtime 如何管理 KV Block

CANN Runtime 在 8.0 后原生支持 Paged KV Cache。管理流程:

  1. 初始化。 在模型加载时分配一个 KV Block Pool——从显存中划出一片区域,切成固定大小的 Block
  2. 分配。 解码阶段每步需要新的 Cache 空间时,从 Pool 中取一个空闲 Block
  3. 页表更新。 把新 Block 的物理地址加入页表,更新逻辑到物理的映射
  4. 释放。 推理结束后 Block 返回 Pool
# Paged KV Cache 初始化
cache_config = {
    "block_size": 16,        # 每个 Block 16 个 Token
    "max_blocks": 100000,    # Pool 总 Block 数
    "enable_paged": True,    # 开启分页
}

Block 的大小可以配置。Block 越大,页表项越少(查询开销低),但内部碎片浪费更多。实测中 block_size=16 或 32 是平衡点。


大模型长文本推理场景

PagedAttention 在长文本推理中的显存优势:

场景 标准 Cache Paged Cache 节省
短对话 (n=128) 3.2GB 0.1GB -97%
中等长度 (n=1024) 3.2GB 0.8GB -75%
长文本 (n=4096) 3.2GB 3.2GB 0%
8×批处理 (n=1024) 25.6GB 6.4GB -75%

短序列和批处理场景的节省最明显。8 批短对话的 Cache 从 25.6GB 降到 6.4GB——一张卡可以装下更多并发请求,服务吞吐翻倍。

Paged KV Cache 的页表管理

Paged KV Cache 的页表存储在 NPU 显存中。每张卡的页表大小 = 页表项数 × 页表项大小。

Block 大小=16 Token 时,max_length=4096 的序列需要 256 个 Block。页表项 = 256 项。每项 8 字节——页表总大小 2KB。40 层 × 40 头 × 2(K+V)= 3200 个页表——总页表大小约 6.4MB。相比于 KV Cache 的几 GB,页表开销完全可忽略。

Runtime 对页表的访问是硬件加速的——Paged Attention 的 DMA 从页表查找物理地址只占 Attention 计算时间的 0.5% 以下。

Block 大小的选择

Block 大小的选择影响 Paged KV Cache 的效率:

Block 大小 页表项数 内部碎片 显存利用率
8 512 4% 96%
16 256 6% 94%
32 128 10% 90%
64 64 18% 82%

Block 越小显存利用率越高(碎片少),但页表项数多。CANN 8.0 的默认 Block 大小是 16——在碎片率和页表大小之间取了平衡。

参考仓库

PagedAttention 实现

FlashAttention 融合优化

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐