大模型推理服务的吞吐,很多时候不是被算力卡住的,而是被显存卡住的。而显存之所以不够用,罪魁往往不是模型权重,是 KV 缓存。PagedAttention 借用了操作系统里一个几十年的老主意——分页——把这块显存管得干净利落。

KV 缓存是怎么把显存吃光的

自回归解码时,每生成一个新 token,都要用到前面所有 token 的 Key 和 Value。为了不重复计算,系统会把它们缓存下来,这就是 KV 缓存。它随序列长度线性增长,而且每条并发请求各有一份

传统服务系统为了让注意力核函数能连续读取,会给每条请求预留一整段连续显存,大小按「最大可能长度」算。于是问题来了:

  • 内部碎片:一条请求实际只生成了几十个 token,却按上千 token 的上限预留了空间,中间那一大段全是「留着但没用上」的浪费。
  • 外部碎片:请求有长有短、来了又走,显存被切得七零八落,剩下的空洞单个都太小,新请求哪个也塞不进去。

三条请求各按最大长度预留连续显存,实际只用了开头一小段,其余是预留未用的内部碎片;请求之间还留下装不下新请求的零散空洞

图 1:连续预分配导致大量显存被「预留但未使用」。可并发的请求数被压低,吞吐也就上不去。

浪费的显存本可以用来容纳更多并发请求。请求并发数上不去,GPU 的吞吐自然也就上不去。

分页:逻辑连续,物理离散

vLLM 的 PagedAttention 把思路整个换掉。它不再要求一条请求的 KV 缓存在物理上连续,而是切成固定大小的 KV 块(比如每块放 16 个 token)。这些块在显存里可以散落各处,一张块表负责记录「这条请求的第几个逻辑块,对应物理显存里的哪一块」。

块按需分配:请求生成到哪,就分配到哪。于是浪费只剩下最后一个没填满的块——内部碎片被压到一个块以内,外部碎片直接消失。注意力核函数则改成通过块表去「按图索骥」,从离散的物理块里把 KV 收集起来参与计算。

一个序列的逻辑块 0–3 通过块表映射到物理显存里离散的块 #7、#3、#9、#1;下方对照操作系统的虚拟页、页表、物理内存

图 2:块表把逻辑上连续的 KV 块,映射到物理上离散的显存块——和操作系统的页表如出一辙。

它其实就是虚拟内存

这套机制熟悉操作系统的人会一眼认出来,它几乎是虚拟内存分页的逐项对应:

操作系统PagedAttention
进程一条推理请求
虚拟页逻辑 KV 块
页表块表
物理内存GPU 显存
按需调页按需分配物理块

好处也跟着一起继承过来了。最漂亮的是共享:多条请求如果有相同前缀(同一段系统提示、并行采样的多个候选、束搜索的多条分支),它们的逻辑块可以指向同一个物理块,用引用计数管理;一旦某条分支要写入、内容开始分叉,才复制出一份——也就是写时复制(copy-on-write)。这在批量生成、Agent 大量复用同一段提示词的场景下,能再省下可观的显存。

本文只讲机制。PagedAttention 带来的吞吐提升取决于模型、序列长度分布、块大小和硬件,各家基准测法不同,这里不引用具体倍数——真要评估,请在你自己的负载上测。

该记住的一点

比起「又发明了一个新算法」,PagedAttention 更像是一次恰当的类比迁移:显存紧张、需要在离散资源上做动态分配——这正是操作系统几十年前就解决过的问题。识别出「这题我在别处见过」,往往比从零硬想更值钱。