04. 显存账本:权重、KV 与工作区
上一篇知道了 KV Cache 用显存换计算。这一篇把账算清楚:一张 80 GB 的 H100 上,模型参数占多少,一个请求的 KV Cache 占多少,剩下的空间能同时服务多少个对话。读完你应该能拿到任何一个模型的配置文件,手算出它每个 token 的 KV 开销,并解释为什么现代大模型几乎都用 GQA。
显存里放了三类东西
显存可以想成 GPU 的工作台。上面放着三类东西:
- 模型权重:所有请求共用的一份参数,加载后常驻显存,大小固定。
- KV Cache:每个请求各自的历史状态,随上下文长度和并发人数增长。
- 运行时工作区:计算过程中的临时结果(激活值)、CUDA 运行环境、通信缓冲区等。
关键的不对称是:权重只有一份,所有人共用;KV Cache 每人一份,人越多、对话越长,占得越多。 一个人用得好好的模型,全公司一起用时显存不够,原因几乎总在 KV Cache。
第一笔:权重
权重的大小很好算:参数个数乘以每个参数的字节数。
text
权重显存 = 参数个数 × 每个参数的字节数
Llama-3.1-8B,BF16:80.3 亿 × 2 字节 ≈ 16.1 GB ≈ 15.0 GiBBF16 和 FP16 每个数占 2 字节,FP8 和 INT8 占 1 字节,INT4 占半个字节。同一个模型换成 INT4,权重只要约 4 到 6 GB(取决于词表相关的层是否保留 16 位),这是第 14 篇量化要讨论的事。
这里顺便说明单位。GB 按 1000 进位(10⁹ 字节),GiB 按 1024 进位(2³⁰ 字节),两者差约 7%。显卡厂商标的 80 GB 和系统工具显示的数字常常对不上,就是这个原因。下面的 KV 计算用 KiB、GiB,因为它们正好是 2 的幂次。
第二笔:每个 token 的 KV
KV Cache 要为每个 token、每一层、每个 KV 头,各存一份 K 向量和一份 V 向量。逐项相乘就是单个 token 的开销:
text
单 token KV 字节数 = 2 × 层数 × KV 头数 × 每头维度 × 每个数的字节数各项的含义:
- 2:K 和 V 两份。
- 层数:每一层都有自己的注意力,都要存。
- KV 头数:注意力被拆成多个头,每个头有自己的 K、V。注意这里是 KV 头的数量,后面会看到它可以少于 Query 头。
- 每头维度:每个头的向量长度。
- 字节数:BF16 为 2,FP8 为 1。
代入 Llama-3.1-8B 的配置(32 层、8 个 KV 头、每头 128 维、BF16):
text
2 × 32 × 8 × 128 × 2 = 131,072 字节 = 128 KiB每多一个 token,就多占 128 KiB。 再乘以长度和并发数:
| 场景 | 计算 | KV 占用 |
|---|---|---|
| 一次短问答,1000 token | 1000 × 128 KiB | 约 125 MiB |
| 带整本手册,8192 token | 8192 × 128 KiB | 1 GiB |
| 32 人同时带手册提问 | 32 × 1 GiB | 32 GiB |
| 一个 128K 长上下文请求 | 131,072 × 128 KiB | 16 GiB |
最后一行值得注意:Llama-3.1 支持 128K 上下文,一个用满长度的请求,KV Cache 就和整个模型的权重一样大。
一张 80 GB 的卡能装下多少对话
把三笔账放在一起。推理引擎通常不会用满全部显存,而是用一个比例参数(vLLM 叫 gpu_memory_utilization,近期版本默认 0.92)给其他程序和意外波动留余量。H100 实际可用约 79.6 GiB,这里保守地按 0.9 估算,约 71.6 GiB(按 0.92 算会多出约 1.6 GiB):
text
可用显存 ≈ 71.6 GiB
− 权重 ≈ 15.0 GiB
− 运行时工作区 ≈ 几 GiB(引擎启动时实测,与批大小等配置有关)
= 留给 KV Cache ≈ 50 GiB 左右50 GiB ÷ 128 KiB ≈ 40 万个 token。这是整张卡在同一时刻能保存的 KV 总量,所有正在进行的请求共享它:
- 每个对话 1000 token,约能同时容纳 400 个;
- 每个对话 8192 token,约 50 个;
- 每个对话 128K,只能容纳 3 个。
引擎启动时会打印类似“GPU KV cache size: N tokens”的信息,就是这笔账算出来的结果。能同时服务多少人,首先由 KV 容量决定,而不是由算力决定。 下面改变文档长度和使用人数,看哪一块在变:
一张 48 GiB 的“工作台”
共享权重 14.90 GiB请求 KV 0.13 GiB其他预留 4.00 GiB
权重加载一份;这条短上下文只占少量计算便笺。
19.03 / 48 GiB,原始预算可容纳
工作台类比对应显存容量。假设 8B BF16 权重,每 token KV 128 KiB,各请求不共享前缀;4 GiB 为手动预留。竖线是容量上限,超出部分仍显示在比例条内。实际峰值还要测量。
GQA:让多个头共用一份 KV
公式里的 KV 头数是线性因子,砍掉它就能直接省显存。早期模型用的是多头注意力(MHA),每个 Query 头配一套自己的 K、V。如果 Llama-3.1-8B 的 32 个 Query 头都有自己的 KV 头:
text
2 × 32 × 32 × 128 × 2 = 512 KiB / token,是现在的 4 倍
4096 token 的请求要 2 GiB,50 个并发要 100 GiB,单张 80 GB 的卡直接放不下分组查询注意力(GQA)让若干个 Query 头共用一组 K、V。Llama-3.1-8B 把 32 个 Query 头分成 8 组,每组 4 个头共享一组 KV,KV Cache 就降到了 MHA 的 1/4。走到极端,所有 Query 头共用一组 KV,叫多查询注意力(MQA),KV 最小,但模型质量损失更明显。
GQA 在模型质量上几乎没有损失,却让同一张卡能多服务好几倍的长对话,所以现在的主流开源模型基本都采用它。看一个模型的推理成本时,num_key_value_heads 这个配置项比参数量更能说明 KV 压力。
不适用这个公式的结构
上面的公式假设每一层都是标准注意力,并且保留全部历史。有几类结构需要另算:
- MLA(多头潜在注意力,DeepSeek-V2/V3 使用):不直接缓存每个头的 K、V,而是缓存一个压缩后的潜在向量,计算时再还原。DeepSeek-V3 每个 token 每层只存 512 维的压缩向量加 64 维的位置编码部分,共 576 个数;61 层、BF16 下每个 token 约 68.6 KiB,比参数量小得多的 Llama-3.1-8B 还少。
- 滑动窗口注意力:部分层只看最近若干个 token,超出窗口的 KV 可以丢弃,这些层的 KV 不再随上下文无限增长。
- 混合模型(注意力层与 Mamba 等线性层交替):线性层保存的是固定大小的递归状态,只有注意力层的 KV 随长度增长。
- MoE 模型:专家只影响权重那一笔账,总参数都要放进显存(或分到多张卡上),KV 仍按注意力层的配置计算。
拿到一个新模型时,先读它的 config.json,确认注意力类型、层数和 KV 头数,再决定用哪个公式。
动手算自己的模型
把你要部署的模型配置填进去,看看权重、KV 和预留各占多少:
显存预算计算器常规全历史 MHA / GQA / MQA
原始权重 14.9 GiB原始 KV 32 GiB其他预留 4 GiB
合计 50.9 / 48 GiB · 超出预算
单序列每 token 128 KiB。权重与 KV 均为原始位数估算,不含量化 scale、分页尾块等;“其他预留”由你填写,不是自动测量。独立序列不共享前缀,不按 GPU 数自动相除。
计算器里的数字是静态估算。实际部署时,引擎启动日志里的 KV 容量才是准确值,因为工作区大小取决于最大批大小、CUDA Graph 等配置,只能在启动时实测。
账算完了,还有一个隐蔽的浪费
前面假设 40 万个 token 的 KV 容量可以被充分利用。实际上,早期的推理系统做不到:因为不知道每个请求会生成多长,它们按最大长度为每个请求预留一整块连续显存。一个请求实际只生成了 100 个 token,却按 2048 预留,剩下的空间全部空占;不同请求的预留块之间还会留下拼不起来的零碎空隙。
这种浪费有多严重?vLLM 的论文统计过当时的系统,KV Cache 显存中真正存放有效数据的比例只有 20% 到 38%。也就是说,账面上能装 400 个对话的卡,实际只能装八十到一百五十个。第 8 篇的 PagedAttention 就是为了把这部分浪费找回来。
显存的三笔账里,权重固定、所有人共用;KV Cache 按 2 × 层数 × KV 头数 × 每头维度 × 字节数 随 token 线性增长,决定了一张卡能同时服务多少对话;GQA 通过让多个头共享 KV,把这笔账砍到几分之一。知道了显存花在哪里,下一篇换个角度,看怎样衡量推理服务的“快”:TTFT、TPOT 与 Goodput。
