用大模型连续对话时,服务端要记住之前说过的每一句,这些记忆就是 KV 缓存。PagedAttention 借了操作系统内存分页的想法来管它,让缓存不再被提前锁死一大块,长对话和多并发都能松口气。
缓存为什么会越占越大
KV 缓存按序列长度预先分配:一个请求来了,系统按最大可能长度给它划一整条连续空间,哪怕它只说到一半,剩下的也空着。进来的是请求序列,发生的是按上限预留,出去的是大片用不上的空洞。
对话越长、并发越多,空洞加起来能把一张显卡吃光。这不是模型算不动,是分配方式太粗:把「可能要」当成「一定有」,显存就这样被预支了。更糟的是空间不连续,明明总余量够,却因为找不到一整条连续块而分配失败。
预支的显存不会自己回来:请求没跑满就释放的空间,如果没有合适的后来者接上,就成了长期占位的空洞。还有一个隐性成本:连续空间分配失败后,系统要么把已用数据搬到一起腾地方,要么直接拒绝请求,两者都伤体验。
分页管理的四个环节
第一步切页。KV 缓存切成固定大小的块,一块只装若干 token 的键值,类似内存分成一页一页。进来的是连续逻辑序列,发生的是按块切分,出去的是编号的物理块。
第二步建表。每个请求维护一张页表,记录自己的第几块逻辑数据落在哪个物理块。进来的是块编号,发生的是逻辑到物理的映射,出去的是可查的页表。
页表查找很快,但也不是零成本,块太小则页表项太多,查表本身也成负担。块的大小要在碎片零头和寻址开销之间取平衡。
第三步按需分配。请求长出来了才要新块,不按上限预支;生成结束,页表一注销,物理块回收。进来的是实时长度,发生的是增量申请与即时归还,出去的是被反复利用的显存池。
第四步共享。多个请求共用同一段前缀时,同一组物理块被多个页表指向,复制时不复制数据、只加映射。进来的是相同前缀,发生的是引用计数,出去的是省下来的整段缓存。
四个环节的动作与产出,对照着看。
| 环节 | 具体做法 | 进入的是什么 | 产出的是什么 |
|---|---|---|---|
| 切页 | KV 缓存切成固定大小的块,一块只装若干 token 的键值 | 连续的逻辑序列 | 编号的物理块 |
| 建表 | 每个请求维护一张页表,记录第几块逻辑数据落在哪个物理块 | 块编号 | 逻辑到物理的映射页表 |
| 按需分配 | 请求长出来才要新块,生成结束页表注销、物理块回收 | 实时长度 | 被反复利用的显存池 |
| 共享 | 相同前缀的同一组物理块被多个页表指向,只加映射不复制数据 | 相同前缀 | 省下来的整段缓存 |
四步走完,缓存管理从「一整条连续空间」变成「一堆可编号、可回收、可共享的小块」。页表本身也占显存,但相比省下的空洞是零头。
碎片与共享的账
分页把「连续大块」变成「固定小块」,内部碎片只剩每页最后一两个 token 的零头,外部碎片几乎被页表吸收,分配失败的概率随之大降。共享则把系统提示词、长文档开头这类公共前缀的复制成本降到接近于零。
共享也不是万能:前缀相同的判定本身有开销,差异出现在中后段的对话,共享比例会迅速下降,这时候省下的主要靠按需分配。两者各管一段,合起来才是完整的省法。
你在 Kimi、腾讯元宝里开多个长文档对话不觉得卡,服务端能同时接住更多请求,这类缓存管理是重要一环。它不改变计算结果,只改变缓存的分配与查找方式。
- 速查:按上限预留是浪费的根源;切页加页表把连续空间变离散管理;按需分配消灭空洞;前缀共享省重复复制。
缓存只是推理服务的一环,配合它工作的还有FlashAttention 是怎么省显存的?分块计算、语义缓存是怎么判断「相似问题」的?和负载均衡是怎么分流请求的?多实例配合。
换个问法再问PagedAttention三句
- 它和显存碎片是什么关系?:核心就是治碎片,按页分配后残留的零头很小,页表把离散块重新缝成逻辑连续。
- 共享前缀会不会写坏?:公共块按只读方式引用,各自的新增内容写在自己的物理块里,互不影响。
- 页大小怎么选?:在寻址开销和碎片零头之间取平衡,具体配置以所用推理引擎的官方文档为准。


