3、PagedAttention原理(上):虚拟内存分页思想、KV Cache的痛点

聊到vLLM,绕不开的核心就是PagedAttention。说实话,我第一次看到这个方案时,第一反应是——这不就是操作系统里的虚拟内存分页吗?后来仔细一读论文,发现还真是。这帮人把OS里玩了几十年的老技术,搬到了大模型推理上,效果出奇的好。

今天我们就来拆解一下,PagedAttention到底解决了什么问题,以及它背后的核心思想。

3.1 KV Cache的痛点:显存去哪儿了?

先说说KV Cache。做过大模型推理的朋友都知道,自回归生成时,每算一个新token,都要用到之前所有token的Key和Value。如果不缓存,每次重新算,那计算量是O(n²),谁也扛不住。

所以业界做法是:把之前算好的K和V存起来,这就是KV Cache。

但问题来了——KV Cache太吃显存了。我给大家算笔账:

模型规模 层数 头数 每头维度 单序列KV Cache大小
7B 32 32 128 约1.5GB(4K长度)
13B 40 40 128 约3GB(4K长度)
70B 80 64 128 约15GB(4K长度)

你看,70B模型一个序列就要15GB。一块A100才80GB,你算算能同时跑几个序列?

核心矛盾:KV Cache是动态增长的,但显存分配是静态的。你没法提前知道每个序列最终会生成多长,这就导致了严重的显存浪费。

3.2 传统KV Cache管理方式:预分配与浪费

在PagedAttention出现之前,主流框架怎么管理KV Cache?

我见过两种做法:

  • 预分配最大长度:每个序列一开始就分配好最大可能长度的显存。比如设置max_seq_len=4096,那每个序列都占满4K的KV Cache空间。但实际很多请求只生成几十个token,浪费率高达90%以上。
  • 动态扩容:序列变长时,重新申请更大的显存块,把旧数据拷贝过去。这种方式虽然节省了显存,但引入了大量内存拷贝和碎片化问题。

我曾经在一个项目里用过动态扩容方案。刚开始觉得挺聪明,结果跑起来发现——显存碎片化严重到令人发指。跑几个小时,显存里全是大小不一的空洞,新序列申请显存时经常失败,但总显存明明还有不少空闲。

避坑提醒:显存碎片化不是小问题。我见过有团队因为碎片化导致推理吞吐量下降40%以上。你以为显存够用,实际上能用的连续空间远小于总量。

3.3 虚拟内存分页思想:从OS偷师

操作系统怎么解决内存碎片化?虚拟内存+分页。

虚拟内存把物理内存切成固定大小的页(通常是4KB),每个进程看到的是连续的虚拟地址空间,但背后映射到不连续的物理页。这样,即使物理内存碎片化严重,进程依然能使用连续的内存空间。

PagedAttention的核心思想一模一样——把KV Cache切成固定大小的块(称为Page或Block),每个块可以独立分配、独立管理。逻辑上连续的KV Cache,在物理显存里可能分散在不同的页上。

你想想看,这解决了什么问题?

  • 不再需要为每个序列预留连续的大块显存
  • 按需分配,用多少分配多少
  • 碎片化的显存也能被充分利用

3.4 PagedAttention如何解决显存碎片化

具体怎么做?我画了一张图,帮你理解PagedAttention的显存管理逻辑:

PagedAttention 显存管理示意 逻辑KV Cache(连续) Token 0 - Token 3 Token 4 - Token 7 Token 8 - Token 11 逻辑上连续,按4个token为一页 物理显存(碎片化) 物理页 3(空闲) 物理页 7(已用) 物理页 2(已用) 物理页不连续,但通过页表映射 页表映射 PagedAttention 核心优势 1. 按需分配:每个序列只分配实际需要的页数,不预占空间 2. 消除碎片:碎片化的显存页也能被分配,利用率大幅提升 3. 共享支持:多个序列可以共享相同的物理页(如Beam Search) 4. 零拷贝:页表更新即可完成显存管理,无需移动数据 5. 显存利用率:从传统方案的20-40%提升到95%以上

说白了,PagedAttention就是给KV Cache加了一层地址映射。逻辑上连续的Cache,在物理显存里可以东一块西一块。你只需要维护一张页表,就能知道每个逻辑页对应哪块物理显存。

3.5 页大小怎么选?

这里有个工程细节——页大小怎么定?

页太小,页表会很大,管理开销高。页太大,又会出现内部碎片(一个页没用满)。

vLLM默认的页大小是16个token。为什么是16?我个人的理解是:

  • 16个token的KV Cache大小适中,页表不会太大
  • GPU的并行计算粒度通常是16或32的倍数,对齐后计算效率更高
  • 内部碎片控制在可接受范围内

小技巧:如果你跑的是超长序列(比如32K以上),可以考虑把页大小调大到32或64,减少页表大小。但短序列场景下,16是个不错的平衡点。

3.6 与传统方案对比

我整理了一个对比表,方便你直观感受差异:

维度 传统预分配 传统动态扩容 PagedAttention
显存利用率 20-40% 50-70% 95%+
碎片化问题 无(预分配) 严重 几乎无
内存拷贝开销 无 高(频繁拷贝) 无(页表更新)
支持序列共享 不支持 不支持 原生支持
实现复杂度 低 中 高

你看,PagedAttention在显存利用率上几乎是碾压式的优势。代价是实现复杂度高了点,但这点代价换来的收益,绝对值。