35GB量化模型为何撑爆80GB GPU?KV Cache显存真相

推理显存由权重、KV Cache、激活和运行时四部分构成,长上下文下KV Cache才是真正的OOM杀手。
文章纠正了"70B模型4-bit量化只需35GB显存"的常见误判。真实显存由模型权重、KV Cache、激活缓冲和CUDA运行时四部分组成,其中权重只是静态地板线。4-bit量化实际有效位宽约4.5–4.85 bpw,70B模型权重实际约39.8GiB。KV Cache在长上下文和高并发下可占显存的45%–65%,一个70B GQA模型在64K上下文、batch=2、FP16缓存下KV Cache即达39GiB,叠加权重后总需求约83GiB,直接OOM。DeepSeek的MLA架构通过将每层KV缓存压缩为576个元素,相比GQA削减约70%–80%显存。生产环境可通过FP8 KV Cache量化、限制max-model-len、开启chunked prefill三项措施防范OOM。
把一个70B模型量化到4-bit,按"0.5字节/参数"的粗算只需要约35GB显存,理论上塞进一张80GB的A100/H100绰绰有余。但真正跑起vLLM或llama.cpp、开一个64K上下文窗口时,torch.OutOfMemoryError: CUDA out of memory 会准时出现。

问题的根源在于:模型权重只是LLM显存的静态地板线。显存里真正的大户是KV Cache,其次是量化scale开销、预填充激活缓冲和CUDA运行时预留。把这些加在一起,真实占用可以是权重的两三倍。
推理显存的四个组成部分
运行时GPU显存由四块独立分配构成:
Total VRAM = M_weights + M_kv_cache + M_activations + M_runtime_overhead
| 组件 | 缩放因子 | 短上下文占比 | 128K上下文/Batch16占比 |
|---|---|---|---|
| 模型权重 | 参数量 × 有效位宽 | 75%–85% | 30%–45% |
| KV Cache | Batch × 上下文长度 × 层数 | 5%–15% | 45%–65% |
| 激活缓冲 | Batch × Prefill块 × 隐层维度 | 3%–8% | 5%–10% |
| CUDA/运行时开销 | 固定值 + PagedAttention块表 | 1.5–3.0 GB | 2.0–4.0 GB |
上下文越长、并发越高,KV Cache的占比就越恐怖。在128K上下文、batch为16的场景下,它已经吃掉了近三分之二的显存。这解释了为什么"权重装得下"和"跑得起来"完全是两件事。
4-bit量化从来不是精确的0.5字节
"INT4就是0.5字节/参数"这个经验法则在生产环境里并不成立。权重分布存在极端离群值,朴素均匀量化会直接摧毁模型困惑度。
主流量化格式(AWQ、GPTQ、GGUF K-Quants)的做法是把权重分组(常见group_size为32/64/128),每组存一个高精度FP16的scale和零点,同时embedding和lm_head层保持16-bit或8-bit精度。实际的有效位宽(bpw)远高于4:
- FP16 / BF16:16.0 bpw(2.0字节/参数)
- FP8 / INT8:约8.5 bpw(含缩放因子,约1.06字节/参数)
- INT4 AWQ(group_size=128):约4.5 bpw(约0.5625字节/参数)
- GGUF Q5_K_M:约5.7 bpw(约0.71字节/参数)
- GGUF Q4_K_M:约4.85 bpw(约0.606字节/参数)
以一个70.6B模型为例,用GGUF Q4_K_M格式,光权重就是约39.8 GiB,比拍脑袋的35GB高出近5GB。这5GB看起来不起眼,但加上后面要讲的KV Cache,就是压垮80GB的最后一根稻草。
静默杀手:KV Cache的精确数学
自回归生成时,如果每步都重算所有历史token的Key和Value,解码复杂度会退化到O(N²)。所以推理引擎把每层的K、V张量缓存起来。对于使用分组查询注意力(GQA)的模型(如Llama、Qwen、Mistral系列),KV Cache字节数为:
KV_Bytes = 2 (K and V)
× n_layers
× n_kv_heads
× head_dim
× seq_len
× batch_size
× bytes_per_kv_element
代入一个70B GQA模型的真实架构常数(n_layers=80,n_kv_heads=8,head_dim=128,FP16缓存即2字节):
- 单token缓存:2 × 80 × 8 × 128 × 2 = 327,680字节 ≈ 320 KB/token
- 64K上下文 × batch=2:320 KB × 65,536 × 2 ≈ 39.06 GiB
现在把这笔账加总:
- 权重(Q4_K_M):39.8 GiB
- KV Cache(64K × batch2,FP16):39.1 GiB
- 激活 + CUDA运行时:约4.2 GiB
- 合计:约83.1 GiB → 80GB的A100/H100瞬间OOM
关键对比:如果这个模型用的是经典多头注意力(MHA,64个KV头)而不是8头的GQA,同样的KV Cache会膨胀到312.5 GiB——这已经不是单卡能装下的问题了。GQA的存在本身就是为了把KV Cache从灾难级压低到可用级,但即便压过之后,长上下文依然凶猛。
分组查询注意力(GQA) 是MHA和MQA之间的一种折中注意力架构。在经典MHA中,每个注意力头都有独立的Query、Key、Value投影,因此KV头数等于Query头数(如Llama-2 70B为64个KV头);在MQA中,所有Query头共享同一组K、V,KV头数降为1。GQA则把Query头分成若干组,每组共享一组K、V,典型配置为8个KV头(每8个Query头共享一组)。由于KV Cache大小与n_kv_heads成正比,GQA把KV Cache相对MHA压缩了约8倍,同时保留了比MQA更好的建模能力,因此成为Llama 2/3、Qwen、Mistral等主流开源模型的标准选择。
DeepSeek MLA如何改写这条曲线
DeepSeek广告-V3和DeepSeek-R1采用多头潜在注意力(MLA)。它们的总参数量是671B的MoE(激活37B),如果按标准GQA在61层上展开128K上下文,光KV Cache的PCIe/NVLink带宽就会成为瓶颈。
MLA的思路是把Key和Value联合投影到一个压缩的低秩潜向量(c_t^KV,典型维度d_c=512),再附加一个解耦的RoPE key维度(d_R=64)。每层缓存的不再是2 × n_kv_heads × head_dim个元素,而只有512 + 64 = 576个元素:
if (arch.attentionType === 'MLA') {
const compressedDim = (arch.kvLoraRank ?? 512) + (arch.qkRopeHeadDim ?? 64);
// MLA每层只缓存联合潜向量,没有K/V的2倍乘数
return arch.numLayers * compressedDim * arch.kvBytesPerElem;
}
注意这里没有GQA公式里的2×乘数,也没有n_kv_heads这个量级项。结果是KV Cache内存相比等效GQA模型削减约70%到80%。据多个独立技术分析来源的共识,这并非营销数字,而是MLA架构设计带来的直接内存收益——这也是DeepSeek能在单节点H800上支撑长上下文的关键原因之一。
多头潜在注意力(MLA) 与GQA的核心区别在于缓存对象的抽象层级:GQA仍在每个KV头上缓存完整的K、V张量,只是减少了头的数量;MLA则把每层的Key和Value联合投影到一个低秩潜向量中(典型维度512),推理时只缓存这个压缩向量加上一个解耦的RoPE维度(64),需要时再通过上投影矩阵还原出K、V。这样每层的缓存单元从2 × n_kv_heads × head_dim(GQA通常为2×8×128=2048)降到d_c + d_R(576),且不存在2倍K/V乘数。需要注意的是,MLA的额外计算开销来自上投影矩阵,属于典型的"以算力换显存"设计,在长上下文场景下收益显著,但在短序列场景可能不如GQA高效。
三个生产环境避免OOM的杠杆
即便架构不在你手中,部署侧仍有明确的降显存手段:
1. 把KV Cache量化到FP8(vLLM中的--kv-cache-dtype fp8)。把每元素字节数从2(FP16)降到1(FP8 E4M3),KV Cache占用直接砍半。在Hopper/Ada架构GPU上,精度损失几乎可以忽略。
2. 用实际P99请求长度卡死--max-model-len。模型支持128K上下文,不代表vLLM就该为128K预分配块表。如果你的P99请求在16K以内,把上限压到16K能省出大量空间。
3. 开启--enable-chunked-prefill。没有分块预填充时,单个64K token的prompt会在prefill阶段让中间MLP激活张量瞬间飙升。分块预填充把峰值激活内存限制在每步固定的token预算内。
这三招配合使用,再叠加MLA这类架构层面的改进,才是长上下文高并发推理能稳住显存的完整答案。显存规划不是"权重能装下"那么简单,而是要把KV Cache当成一等公民来算。
你目前的本地或生产推理是用什么GPU配置?有没有在长上下文负载下测试过FP8 KV Cache量化?欢迎在评论区交流。
分块预填充(chunked prefill) 的工作原理是把一个长prompt切分成多个固定大小的token块(如512或1024 token),逐块执行prefill并同时与decode请求混合调度,而不是一次性把全部prompt塞进前向传播。其收益有两层:一是把prefill阶段的峰值激活内存从O(prompt长度)降为O(chunk大小),避免64K prompt瞬间撑爆显存;二是让长prefill不再阻塞decode请求,显著降低TTFT和ITL抖动。vLLM从0.4版本开始支持该特性,但开启后单步计算图中会同时包含prefill和decode两种workload,可能对某些自定义kernel的兼容性提出额外要求,生产部署前需做吞吐回归对比。
相关推荐

“黑色十月”:AI挖出史上3%严重漏洞?
Reddit 热帖称过去一个月发现了史上 3% 的严重软件漏洞,被称为「黑色十月」。本文拆解这一说法的统计口径问题,分析 AI 辅助代码审计如何加速漏洞发现,以及开发者和企业该如何应对补丁窗口缩短的挑战。

微软Project Gecko:用AI为非洲农民架起语言桥梁
微软Project Gecko项目推出PharmaChat应用,让非洲农民用母语提问,通过AI返回精准视频片段和农耕书籍章节。项目还发布三个交互式手册,推动包容性AI发展。

Node.js语音笔记App:语音转文字API选型指南
为Node.js语音笔记应用选择语音转文字API?对比AssemblyAI、Deepgram、Google Cloud和Whisper的免费额度、定价与Node集成体验,给出分阶段选型建议。