LLM 生成文本是autoregressive(自回归)的:每次只生成一个 token,然后把它拼回输入,再生成下一个。问题在于 attention 的计算方式——生成第 100 个 token 时,它需要和前面全部 99 个 token 的 Key 和 Value 向量做注意力计算。如果每一步都从头重算这 99 个 token 的 K/V,那么生成 n 个 token 的总计算量是 O(n²) 级别的重复劳动——而这些 K/V 在数学上完全没有变过。
KV cache(键值缓存)就是这个朴素的优化:算过的 K/V 存进显存,下一步直接复用。每生成一个新 token,只为它这一个 token 算 K/V,追加到缓存末尾。代价是——这份缓存要一直占着显存,而且随序列变长线性增长。
每个 token 的 KV cache 大小是一个可以手算的数字,这也是面试里最常被要求现场推导的公式:
800 KB——一个 token。一张 A100-40GB 装下 13B 模型的 FP16 权重(约 26 GB)之后,剩下的显存大部分都要让给 KV cache。这意味着能同时服务多少条请求,瓶颈往往不是算力,而是 KV cache 能塞下多少。这就是为什么显存管理从一个实现细节变成了推理系统的核心命题。
2023 年之前的主流推理框架为每条请求预分配一段连续显存,长度按「最大可能序列长度」(比如 2048)切好。这个决定带来三种浪费:
① 内部碎片 (internal fragmentation)——请求实际只生成了 300 个 token,但按 2048 预留的那段空间里,剩下 1700 多个 token 的位置从头到尾没被用过。② 预留浪费 (reservation)——序列还没长到那里,但空间已经被锁定,别的请求用不了。③ 外部碎片 (external fragmentation)——不同请求的预分配块大小不一,释放后在显存里留下大量塞不进新请求的缝隙。
操作系统在几十年前就解决过一模一样的问题——进程要「看起来连续」的内存,物理内存却是碎的。答案是virtual memory(虚拟内存)+ 分页。PagedAttention 把这套机制搬进了显存:
KV cache 被切成固定大小的 block(vLLM 默认 16 个 token 一块)。每条请求维护一张 block table(块表),记录「逻辑上的第 N 块」实际存放在哪个物理块——物理块可以散落在显存任何位置。序列增长时按需领取新块,请求结束立刻归还。三种浪费里,外部碎片和预留浪费被直接消灭,内部碎片被压缩到「最后一个未填满的 block」,不到 4%。
分页还带来一个意外的礼物:共享。同一个 prompt 发起的多路并行采样(parallel sampling)、beam search 的公共前缀,可以让多条序列的块表指向同一批物理块,配合copy-on-write(写时复制)机制,共享的前缀只存一份。这是后来 prefix caching(前缀缓存)一系列优化的地基。
UC Berkeley 团队在论文里给出的测量:当时的主流系统(FasterTransformer、Orca 等)中,KV cache 显存里真正装着有效 token 数据的比例只有约 20%–38%——其余全部是上文三种浪费。引入 PagedAttention 后:
浪费的显存被释放出来意味着同样的卡能塞下更长的批次——吞吐提升不是来自算得更快,而是来自「一批能同时跑更多序列」。这个案例最值得记住的一点是:2–4 倍的性能提升里没有任何一行 CUDA kernel 的算力优化,纯粹来自内存管理策略的改变。vLLM 随后开源,PagedAttention 成为几乎所有主流推理框架的标配。
vLLM:PagedAttention 的出处与参考实现,生产环境最常见的开源选择;--block-size 可调块大小,--gpu-memory-utilization 控制 KV cache 可用显存上限。
SGLang:在分页基础上引入 RadixAttention,用基数树自动做跨请求的前缀共享,多轮对话 / few-shot 场景收益明显。
TensorRT-LLM / TGI:均已内置 paged KV cache;TensorRT-LLM 在 NVIDIA 硬件上通常有额外的 kernel 级优化。TGI 已进入维护模式、仓库已归档,新部署优先考虑 vLLM / SGLang(见附录 A)。
KV cache 量化:FP8 / INT8 KV cache 已在主流框架落地,与分页正交,可叠加使用(进一步讨论见 1.4 量化)。
套用公式:2 × 32 × 4096 × 2 bytes = 524,288 bytes ≈ 512 KB / token。4096 token 的请求峰值 ≈ 2 GB。
进阶追问(面试常见):如果这个模型用了 GQA(分组查询注意力),比如 32 个 query head 只配 8 个 KV head,上面的数字要乘以 8/32——KV cache 直接降到 1/4。这正是现代开源模型几乎全部采用 GQA 的核心原因之一:它省的不是算力,是 KV 显存。
每条请求各自持有一份独立的 KV cache(不同请求的 K/V 互不相干),所以随并发数线性叠加;每条请求内部又逐 token 追加,所以随序列长度线性增长。总占用 ≈ 并发数 × 平均序列长度 × per-token 大小。
推论:显存装完权重后剩下的空间除以 per-token 大小,就是「全场总 token 预算」。并发和上下文长度在这个预算里此消彼长——这就是为什么长上下文服务的并发上限低,也是容量规划(2.4 节)的起点。
B 获益最大。PagedAttention 消灭的是「按最坏情况预留」造成的浪费——请求长度方差越大、并发越高,预留浪费和碎片就越严重,分页的收益也越大。A 的序列长度固定且已知,预分配几乎没有浪费;C 是 encoder 前向计算,没有自回归增长的 KV cache,分页机制基本无关。
记住这个判断模式:「不确定性越大的地方,按需分配的价值越高」——这和操作系统当年引入虚拟内存的理由一字不差。
它们解决的问题不同但机制上有依赖关系。PagedAttention 解决「一条序列的 KV 怎么存」;prefix caching 解决「多条序列的公共前缀能不能只算一次、只存一份」。
没有分页理论上也能做前缀缓存(连续显存里复制一份前缀 KV),但「共享同一份物理数据」就很难:连续布局下每条序列要求自己的 KV 是一整段连续内存,公共前缀无法被两条序列同时指向。分页 + 块表让「多个逻辑块映射到同一个物理块」变得自然,配合 copy-on-write 处理分叉——所以工程实践里,前缀缓存都是建立在分页机制之上的(应用侧视角见 2.2 Prompt Caching)。