第 14 篇 · 第三部分 · 大语言模型是怎么长出来的
KV Cache 与上下文窗口:推理时显存被什么占用
上一篇讲 RLHF 和 DPO,说的是怎么把模型调整得符合人的偏好。这一篇换到推理这边:发出请求之后,模型开始逐字输出,显存占用也随之一路上升。很多人以为显存全部被模型权重占用,权重确实是主要部分,但还有一项开销随上下文长度线性增长,对话一长就足以占满整张显卡。模型生成是串行的,为了不重复计算,它把历史 K、V 全存了下来——这份缓存正是显存占用的主因。
一、生成是串行的,那为什么要缓存
先说清大模型到底怎么"说话"。它不像人写文章那样能打好腹稿一次输出,它一次只输出一个 token:算出第一个字,把它接到输入后面,再算第二个字,再接上去,依次进行下去。这个流程是串行的,因为第 t 个字的概率分布,依赖前面 t-1 个字。
问题就在于这个"依赖"上。注意力机制要求,生成每个新 token 时都要回头看一遍前面所有的 token。看谁、看多重的依据,是当前 token 的查询 Q 和每个历史 token 的键 K 做点积。于是每生成一步,历史 token 的 K 和 V 都要被用到。
如果这些 K、V 每次都临时重新计算,等于每一步把整段历史重新过一遍投影矩阵。刚刚算过、结果又不会变的量,直接存下来即可。KV Cache 正是这种避免重复计算的思路的产物:把算过的 Key 和 Value 缓存住,新 token 只算自己那一份。
换个角度看,这也是 Transformer 推理和训练最不一样的地方。训练时整句一次性并行算,序列多长都无所谓;推理时只能一个接一个,后一个必须等前一个算完。整条链路受制于"串行"这一点。
二、不做缓存会怎样:K 和 V 被反复重算
这不是节省一点边际开销的问题,量级相差很大。
假设要生成 n 个 token。有缓存时,第 t 步只需给新 token 算一份 K、V,投影部分的计算量是 O(1),n 步加起来 O(n)。没缓存的话,第 t 步要重算前 t 个 token 的 K、V,加起来是 O(n²)。n 取 4096,这两个方案在投影这块差了大概两千倍。
得分清楚:注意力里 Q·KT 那一步,无论有没有缓存都得做——因为 Q 是新的,得跟所有历史 K 逐个打分,这是 n 次点积,躲不掉。能被缓存省下的,是 K、V 的线性投影,也就是乘 W_K、W_V 那两步。缓存消除的是"重复劳动",不是"必要劳动"。
还有一种更不经济的做法:把每个前缀整句重新输入一遍。那就是 O(n³) 了,完全是一种浪费。没有人这么实现,但它的存在能帮你估量出缓存的实际价值。
那两千倍是分摊到整个生成过程上计算的。具体到单独某一步,差别更大:生成第 4096 个字时,没缓存的方案要重新投影 4096 份 K、V,有缓存只投影 1 份,这一步的成本差着四千倍。
三、KV Cache 里到底存了什么
存的东西很朴素:每一层、每一个 KV 头、每一个历史位置上的 K 向量和 V 向量。
每 token 显存 ≈ 2 × L × Hkv × dhead × 字节数这里的 2 是 K 和 V 两份,L 是层数,Hkv 是 KV 头数,dhead 是每个头的维度,字节数在 fp16 下取 2。
换成张量的说法:每一层都存着两份矩阵,形状是「历史长度 × KV 头数 × 每头维度」,一份 K、一份 V。序列每长一格,这两个矩阵就各多一行。推理框架里它们常被叫作 past_key_values,看名字就知道它的用途。
拿一个 7B 模型代入:32 层,32 个注意力头,每头 128 维,fp16。那么每 token 的缓存是 2 × 32 × 32 × 128 × 2 字节 = 524288 字节,约 0.5 MB。
0.5 MB 看似不大,一张手机照片都有两三 MB。可它是每个 token的账,不是整个模型的账。对话越久,占用越多。
要是换用 GQA 的模型(32 个 Q 头、8 个 KV 头),这个数还得再除以 4,每 token 落到 0.125 MB 左右。后面讲优化时会看到,KV 头数是决定这笔开销大小的首要因素。
四、算一笔具体的账:7B、32 层、4096 长度
把那 0.5 MB 乘上长度。上下文 4096 时,KV Cache 约 2 GB。
再看权重:7B 参数,fp16 每参数 2 字节,光权重就 14 GB。两边加起来 16 GB。一张 24 GB 的 RTX 4090,减去这两笔还剩 8 GB 左右,要留给激活值和各种中间张量。
如果同时跑 8 条对话(batch size = 8),KV Cache 涨到 16 GB,加上 14 GB 权重,30 GB,显存直接溢出。这就是为什么本地运行模型时,明明"24G 显存能容纳 7B 模型",一旦多人并发就会 OOM。
还有个容易忽略的点:KV Cache 常按最大长度预分配。推理框架为了简化管理,一次性预留一大块连续显存,按序列可能达到的最长长度算。结果短对话也占用着长对话的显存。PagedAttention 后来要解决的,正是这种浪费。
有人会想:把 max_length 设小、按实际用到的长度分配,是否可行?思路是对的,难点在调度:框架事先并不知道这次请求会聊多长,只好按上限预留。真正的解法要等到把缓存切块、按需分配的路子出现,后面会说。
把同样的算式套到别的尺寸上,感受更直观。13B 权重约 26 GB,KV 缓存若层数头数与前者相当,4096 长度下约 3 GB;而 70B 用了 GQA、KV 头数压到 8 个,单 token 的缓存反倒比 7B 还小,权重却涨到 140 GB。可以看出权重和缓存是两项独立的开销,谁大谁小,取决于具体配置。
五、prefill 和 decode:一个吃算力,一个吃显存带宽
推理其实分两段,性质完全不同。
prefill 阶段,把整段 prompt 一次性输入进去,所有 token 并行做矩阵乘法。这时 GPU 的算力单元接近满载,瓶颈在计算,不在数据传输。上下文越长,这段越耗时,而且注意力那一步是 O(n²),128k 的 prompt 仅打分矩阵一项就相当可观。
decode 阶段,则是逐个 token 输出。每输出一个,都要把整个模型的权重从显存里读一遍,再把整条 KV Cache 读一遍。注意这时计算量很小:只为处理一个 token,却要读取 14 GB 权重。瓶颈从算力变成了显存带宽。
粗略估算上限。A100 显存带宽约 2 TB/s,读 14 GB 权重理论耗时约 7 ms,对应每秒一百多个 token;消费级 4090 带宽约 1 TB/s,同样一笔账要 14 ms,理论上限掉到七八十 token/秒。这跟模型本身强不强没关系,纯粹由显存带宽决定。
decode 阶段还有个特点:单条请求的并行度很低,只有一个 token 需要计算,GPU 的算力单元大半处于空闲。缓解办法是批处理——把多条请求的 decode 拼在一起。权重只读取一次,却提供给多个 token,带宽利用率就上来了,批越大,每个 token 分摊到的权重读取成本越低。代价是显存里要多存放几份 KV Cache,这又回到了开头算过的那笔账。
六、上下文窗口为什么不是越长越好
厂商总是把"支持 128k、1M 上下文"作为卖点。能力确实有用,但代价是实实在在的。
KV Cache 随长度线性增长。还是那个 7B 例子:4096 长度要 2 GB,128k 长度就是 64 GB 左右。一张 80 GB 的 A100 仅缓存就快占满,权重都无处存放。长上下文不是"是否愿意开启"的选项,是"显存是否充足"的硬门槛。
再说计算。prefill 里注意力是 O(n²),长度翻四倍,计算量涨十六倍。有些实现退化得更厉害,因为 KV Cache 一大,访问模式变差,访存效率随之下降。此外还有位置编码的问题:模型训练时见过的长度有限,硬推更长,效果未必线性延续。所以"窗口大"和"窗口有效"是两回事。
也有绕开这笔账的思路。比如滑动窗口注意力,只看最近 N 个 token,更远的直接不缓存——窗口固定,显存就成了常数,代价是模型无法记住太久以前的事。或者把历史压缩成更短的表示,只留要点。这些方案都在"节省多少"和"牺牲多少"之间寻求平衡,是近两年长上下文研究的热点。
数字上的门槛是明确的:70B 的权重 140 GB,一张 80 GB 的卡根本无法容纳,需要拆分到多卡;7B 勉强能够容纳,可 128k 上下文一开,64 GB 缓存又把显存占满。所以有些号称支持超长上下文的模型,实际部署时会把默认长度调回 4k 或 8k。
七、优化手段:MQA、GQA、PagedAttention、KV 量化
既然 KV Cache 是头号开销,围着它的优化自然最多。四类手段,各有侧重。
MQA(多查询注意力):让所有注意力头共享同一组 K、V,只保留各自不同的 Q。Shazeer 在 2019 年提出,缓存直接缩到 1/H。以 32 个头为例,节省 32 倍。代价是质量会下降一些,长序列生成时表现尤其敏感。
GQA(分组查询注意力):折中方案。把 Q 头分成若干组,每组共享一组 K、V。比如 32 个 Q 头配 8 个 KV 头,就是每 4 个 Q 头共用一份 K/V。缓存降到四分之一,质量损失比 MQA 小得多。LLaMA 2 70B 和不少开源模型都用了这套,配置项里直接有 KV 头数和 Q 头数两个数。
PagedAttention:vLLM 的核心。把 KV Cache 切成固定大小的 block 来管理,像操作系统用分页管物理内存那样,用到哪页给哪页,不再要求一整块连续空间。碎片不再产生,多条请求的缓存能混合存放,同样的显存容纳更多并发。
KV 量化:把缓存从 fp16 压到 int8 甚至 int4。内存相应减半到四分之一,精度损失通常可以接受,因为缓存数值的分布相对集中。这是下一篇要讲的量化技术的一个具体应用面。
这四类手段常叠加使用:GQA 作为基础,PagedAttention 管理分配,KV 量化再压缩一级。一个开源的 7B 模型部署成服务能支撑多少并发,基本就看这几项配置如何。
另外纠正一个常见误解:GQA 不只是为了节省显存,它同样让 decode 更快。每步要读的 KV 少了,在带宽受限的解码阶段,这份收益是实实在在的。
八、和后面几篇的衔接
把本篇的内容总结一下:推理时的显存,权重是固定的一笔,KV Cache 是随长度和并发浮动的一笔。前者靠量化压缩,后者靠 GQA、分页、缓存量化压缩。
下一篇顺着权重那笔账往下走——一个 fp16 占 14 GB 的模型,能不能压缩到 4 GB 甚至更小,压缩之后还能不能正常工作。
把这套逻辑梳理清楚,以后再看到"某框架支持百万级 token 上下文"这类宣传,你多半会先追问三件事:KV 缓存要占多少显存、哪张卡容纳得下、decode 速度下降到什么程度。答不上来的,往往还只是纸面数字。
参考资料
- Noam Shazeer, Fast Transformer Decoding: One Write-Head is All You Need, 2019. arXiv:1911.02150(多查询注意力 MQA 的出处)
- Joshua Ainslie 等, GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints, 2023. arXiv:2305.13245(分组查询注意力 GQA)
- Woosuk Kwon 等, Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023. arXiv:2309.06180(vLLM 与分页式缓存管理)
- Reiner Pope 等, Efficiently Scaling Transformer Inference, 2022. arXiv:2211.05102(讨论推理阶段的算力与带宽瓶颈)
- vLLM 官方文档. docs.vllm.ai(配置与部署实践,可用于对照阅读)