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加了一层地址映射。逻辑上连续的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在显存利用率上几乎是碾压式的优势。代价是实现复杂度高了点,但这点代价换来的收益,绝对值。