上篇我把”上下文很贵”拆成了三笔账:账单上的 token 钱、显存里的空间、还有模型”记不住中间内容”的效果损耗。今天拆第二笔——显存到底被谁吃掉了。答案基本是同一个:KV Cache。
起因是我前阵子给朋友部署一个 Llama 3.1 8B 的问答服务,用一张 24GB 的显卡。8B 参数 fp16 权重也就 16GB 左右,我当时觉得 24GB 绰绰有余。结果把上下文长度拉到 128K,直接 OOM。后来一算才明白:权重只是”房租”,KV Cache 才是真正的大头。我是前端出身,手头就一张卡,每一 GB 都得抠着用,所以这篇把”这笔账怎么算、怎么省”一层一层拆开,都是我自己验证过的数字。
一、先算账:KV Cache 是怎么把显存吃光的
自回归生成时,每生成一个 token,模型都要”回看”前面所有 token 的 Key 和 Value,所以每个历史 token 的 K、V 都得常驻显存。而且序列每长一个 token,缓存就线性多一份——这就是为什么 KV 和上下文长度严格成正比。存一个 token 的 KV 占用公式是:
2(K 和 V 两份)× 层数 × KV head 数 × head_dim × 每个元素字节数
Llama 3.1 8B 的架构参数(论文 Table 3):32 层、8 个 KV head,head_dim = 4096 ÷ 32 = 128。fp16 下每个元素 2 字节:
2 × 32 × 8 × 128 × 2 = 131,072 字节 ≈ 128 KB / token
把 128K 上下文塞满:128 KB × 131,072 token ≈ 16 GB。也就是说一条请求的 KV Cache 就能顶上一份 fp16 权重的体积。如果同时并发 4 条请求,就是 4 份,64GB 的卡也扛不住。这也是为什么”上下文贵”不只是钱的问题——显存根本装不下。
二、GQA:从结构上把 KV 砍掉四倍
如果 Llama 3 用标准 MHA(每个 query head 配一个 KV head,32 个),同样的公式算出来是 512 KB/token,128K 要 64GB。GQA(分组查询注意力,Ainslie et al. 2023)的思路是让一组 query head 共用一个 KV head:MQA 只留 1 个 KV head,速度最快但质量掉;GQA 取中间值,Llama 3 全系保留 8 个 KV head,KV Cache 直接除以 4。论文的结论是:质量接近 MHA,速度接近 MQA。
所以我现在选模型会先看 KV head 数:8 个 KV head 的模型(比如 Llama 3 系)天生比 32 个的省 4 倍。这是”结构上”的省,模型训完就固定了,没法后期改。
三、KV 量化:fp16 变 fp8,缓存再减半
结构省完,还能在存储精度上省。KV Cache 默认 fp16(2 字节),量化成 fp8(1 字节)直接减半。我的做法是用 vLLM 部署,启动时加一个参数:
vllm serve meta-llama/Llama-3.1-8B-Instruct --max-model-len 32768 --kv-cache-dtype fp8_e4m3
–kv-cache-dtype 支持 fp8_e4m3、fp8_e5m2 等精度,完整列表和显卡支持情况以 vLLM 官方文档为准。简单说 e4m3 精度更高、e5m2 动态范围更大,我一般先用 e4m3。KV 量化会带来少量精度损失,效果敏感的任务我会上线前先跑一遍评测。按上面的公式验证:32K 上下文时 KV 是 4GB,fp8 后剩 2GB,24GB 卡上 fp16 权重 16GB + 2GB KV,余量一下就出来了。
四、PagedAttention:把碎片内存捡回来
前两层解决”总量”,这一层解决”浪费”。KV Cache 是动态增长的:请求进来时你不知道它会生成多长,传统做法按最大长度预分配连续内存。PagedAttention 论文指出,这种做法存在三类浪费:预留、内部碎片、外部碎片——内存看着还有,但都碎成了没法用的小块。
vLLM 的 PagedAttention 学的是操作系统分页:把 KV Cache 切成固定大小的块,逻辑上连续、物理上可以零散,按需分配,用完即还。配合请求间 KV 共享(比如并行采样时多条输出共用同一份 prompt 的缓存),论文称做到了”近零浪费”,吞吐比 FasterTransformer 和 Orca 高 2-4 倍。Meta 在 Llama 3 论文里也提到用 PagedAttention 做拒绝采样,吞吐提升 2 倍以上。换句话说:同样的显存,能塞进更多并发请求。
五、更激进的路线:MLA
最后提一个更狠的思路。DeepSeek-V2 的 MLA(多头潜在注意力)把每层的 K、V 压缩成一个 latent 向量,推理时只缓存这个向量而不是完整的 K、V,论文给出的数字是:KV Cache 减少 93.3%,生成吞吐提升 5.76 倍。如果你最近在选新模型,值得关注这类从结构上压缩缓存的方案。
六、四层手段对比与我的调显存清单
| 优化手段 | 省多少 | 原理 | 代价 |
|---|---|---|---|
| GQA | 4 倍 | 8 个 KV head 共享 | 需选支持 GQA 的模型 |
| KV 量化(fp8) | 2 倍 | 2 字节 → 1 字节 | 少量精度损失 |
| PagedAttention | 利用率近 100% | 分页式内存管理 | 无(工程实现) |
| MLA | 93.3% | KV 压缩成 latent | 需选 DeepSeek 系架构 |
- 先算账:用上面的公式算出目标上下文长度下的 KV 大小,再决定显存够不够,别凭感觉。
- –max-model-len 按业务设:KV 随上下文长度线性涨,别无脑开满 128K,够用就行。
- –gpu-memory-utilization:控制显存占用比例,vLLM 默认 0.92,我会留一点余量给 CUDA context。
- –max-num-seqs:限制并发序列数,每多一条并发就多一份 KV 缓存。
- 还紧就上 –kv-cache-dtype fp8_e4m3:KV 量化再省一半,是我目前最常用的”最后手段”。
总结
一句话:KV Cache 吃显存是结构性的——GQA 从模型结构砍 4 倍,KV 量化再砍一半,PagedAttention 把碎片捡回来,MLA 则是把整份缓存压缩掉。我的排查顺序是:先套公式算账,再按清单从上到下逐层优化,基本都能在不动模型的前提下把显存抠出来。
下期预告:上篇三笔账还剩”重复输入花钱”没拆——提示缓存(Prompt Caching)怎么做到同一段前缀只算一次钱?上下文压缩又是怎么把长文档压进窗口的?想一起折腾的,下篇见。
参考文献
- Llama Team, AI @ Meta. The Llama 3 Herd of Models(Table 3 架构参数、GQA、PagedAttention 采用):https://arxiv.org/abs/2407.21783
- Ainslie et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints:https://arxiv.org/abs/2305.13245
- Kwon et al. Efficient Memory Management for Large Language Model Serving with PagedAttention:https://arxiv.org/abs/2309.06180
- DeepSeek-AI. DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model:https://arxiv.org/abs/2405.04434
- vLLM 官方文档(–kv-cache-dtype / –gpu-memory-utilization / –max-model-len):https://docs.vllm.ai/en/latest/cli/serve.html
- 上一篇:上下文工程开篇:为什么”上下文”正在成为最贵的资源?:https://www.fedte.cc/p/834.html
转载请注明:Falost的小窝 » KV Cache 是怎么把显存吃光的?GQA、KV 量化、PagedAttention 三层优化实战