3146 字
约 10 分钟
16
S23-B02 · 显存构成:权重、KV Cache 与激活

推理服务的第一个硬约束不是算力,是显存。卡算力不够,最多是慢;显存不够,是直接起不来或者跑一半 OOM。

而"显存不够"几乎从来不是一句"模型太大了"能解释的。同一张卡上跑同一个模型,把上下文从 2K 提到 32K、把并发从 4 提到 64,显存占用能翻好几倍,权重却一个字节都没变。所以必须先知道显存被谁拿走,才能判断该动哪一块。


1. 显存被谁占用

推理进程的显存大致分成四份,性质完全不同:

组成 是什么 随什么增长 可压缩性
权重 模型参数本身,只读常量 参数量 × 精度 高:降精度、换量化格式
KV Cache 每条请求已生成内容的注意力缓存 并发 × 上下文长度 中:压缩、换出、限长
激活与临时缓冲 前向计算的中间张量、通信缓冲 批大小 × token 数 中:批大小、chunked prefill
框架预留 引擎自身的 workspace、CUDA context、显存池碎片 引擎版本、分配策略 低:换引擎参数、预留比例

一个快照拆成这四份,就能对上账:

总占用 ≈ 权重 + KV Cache 实际占用 + 激活峰值 + 框架预留
必须满足:总占用 ≤ 卡显存 × 可用比例     # 留余量,别按 100% 算

关键是权重是常量,KV 是变量。权重决定了"能不能装下这个模型",KV 决定了"装下之后能同时服务几个人"。所有容量问题的本质都是在这两者之间做分配。

2. 权重的估算方法

权重显存 = 参数量 × 每参数字节数
每参数字节数 = 位宽 / 8
精度 每参数字节数 备注
FP32 4 推理基本不用
FP16 / BF16 2 常见基线
FP8 1 需要硬件与引擎同时支持
INT8 约 1 常伴随 scale/zero-point 的少量额外开销
INT4 约 0.5 实际略高,量化元数据与部分保留层会加回一点

按模型 config 与卡规格核对的三个注意点:

  1. 参数量要按 config 数出来,不要用名字里的数字猜(embedding 与 lm_head 是否共享、MoE 的激活参数与总参数不是一回事);
  2. INT4 不等于精确的 0.5 倍,量化元数据、未量化的层(如 norm、部分 attention)、embedding 常常保持高精度;
  3. 权重之外还有加载开销,从磁盘加载到显存的过程会峰值占用,冷启动阶段要单独留量。

3. KV Cache 的估算方法

KV Cache 是每条请求、每一层、每个历史 token 都要存一对 K 和 V。公式:

每 token KV 字节 = 层数 × 2 × KV头数 × 头维度 × 每元素字节数
                 # 2 = K 和 V 各一份;每元素字节数按 KV 的精度(常与权重不同)
单请求 KV 字节 = 每 token KV 字节 × 上下文长度(prompt + 已生成)
总 KV 字节 ≈ 单请求 KV 字节 × 并发序列数
因子 由什么决定 常见误判
层数 模型深度 按 config 的 num_hidden_layers 取
KV 头数 注意力结构 GQA/MQA 下远小于注意力头数,别用 num_attention_heads
头维度 config 的 head_dim 有的模型 head_dim ≠ hidden/heads
每元素字节 KV 精度 KV 量化后与权重精度可以不同
上下文长度 实际输入+输出 用平均值会低估峰值,按 P99 长度算

这里的每一个数都要按模型 config 与卡规格核对。最容易被忽略的是第三条:GQA 模型的 KV 头数可能只有注意力头数的几分之一,直接拿头数代入会高估好几倍,然后得出"这卡跑不了"的错误结论。

4. 为什么长上下文与高并发同时吃 KV

KV 的总量是"长度 × 并发"的乘积,两者一起涨不是相加而是相乘:

场景 上下文 并发 相对 KV 总量
短对话 短 低 1×(基准)
长文档问答 长 低 随长度线性放大
高并发短请求 短 高 随并发线性放大
长上下文 + 高并发 长 高 两者相乘,最先打爆

这就是"上下文一开长,并发就得砍"的原因。反过来说,任何"支持 128K 上下文"的宣传,都要追问一句:在多少并发下支持。单条请求跑满长上下文,和 32 条并发都跑满长上下文,需要的显存差一个数量级。

5. 激活与临时缓冲的量级变化

阶段 激活量的决定因素 量级特征
decode 单步 批大小 × 隐层维度 小,但每步都有
prefill 批内总 token 数 × 隐层维度 × 层数(含中间张量) 大,随 prompt 长度陡增
大 batch prefill 上述再乘批内请求数 最大,OOM 常发生在这里
通信缓冲 并行度、all-reduce 实现 多卡时不可忽略

两个实践结论:OOM 常常发生在 prefill 而不是 decode;限制单批 token 总数(而不是只限批内请求数) 是控制激活峰值最直接的手段。很多引擎用 max_num_batched_tokens 这类参数来管这件事。

6. "装不下"的四种应对与各自代价

应对 做法 收益 代价
降精度 权重 FP16 → INT8/INT4 权重显存大幅下降 质量风险,需要评估集回归
缩上下文 限制最大输入/输出长度 KV 线性下降 长文档类需求直接不可用
减并发 降低并发序列上限 KV 线性下降 吞吐下降、排队变长
加卡 张量并行/流水并行分摊 单卡压力下降 通信开销、成本上升、复杂度上升

顺序建议:先用"减并发"确认是否真的装不下(很多人是在压测脚本里开了不现实的并发),再考虑"缩上下文",然后才是"降精度",最后才是"加卡"。因为前三者都是配置改动,加卡是架构改动。

7. 显存与吞吐的关系

剩余显存直接决定能放多少请求,而能放多少请求决定吞吐上限:

可并发序列数 ≈ (卡显存 × 目标利用率 − 权重 − 激活峰值 − 预留) / 单请求 KV 字节
吞吐上限 ≈ 可并发序列数 × 每序列每秒产出 token 数

注意这是上限,不是实际值:并发上去了,带宽被分薄,每序列的出词速度会下降,实际吞吐通常低于这个乘积。所以"显存越多吞吐越高"只在没到带宽饱和之前成立。饱和之后再加并发,只是让队列变长。

8. 怎么读一个显存占用快照

$ nvidia-smi --query-gpu=memory.total,memory.used,memory.reserved --format=csv
$ nvidia-smi --query-compute-apps=pid,used_memory --format=csv

# 引擎侧通常还有更细的口径(名字按引擎版本核对)
# vLLM: 日志里的 GPU blocks / KV cache blocks、前缀缓存命中
# TensorRT-LLM: build 阶段的 max batch / max tokens 预算

按四份对账的顺序:

步骤 拿到什么 对不上的常见原因
1 memory.total 卡被别的进程占了(训练残留、别的服务)
2 权重字节(按参数量算) 参数量数错、量化后仍按 FP16 算
3 KV 实际占用(引擎日志/块数) 前缀缓存把复用部分也算了进去
4 激活峰值 只在稳态采样,没抓到 prefill 峰值
5 差值 = 框架预留 + 碎片 碎片被当成"泄漏"

最后一行值得强调:显存池碎片会让"理论够用"变成"实际 OOM"。解决办法是看引擎的块利用率指标,而不是反复调小并发去凑。

9. 一个预算表模板

把这张表填满,容量问题基本就清楚了。数字必须来自真实 config 与卡规格:

项 符号 取值来源
模型参数量 P config / 权重文件
权重精度字节 b_w 部署精度
权重显存 P × b_w 计算
KV 每 token 字节 k 层数 × 2 × KV头数 × 头维度 × 字节
目标上下文长度 L 业务 P99 长度
目标并发 C 峰值并发
KV 总需求 k × L × C 计算
激活与预留 A 批大小 × 经验比例,实测校准
单卡需求 P×b_w + k×L×C + A 与卡显存比
剩余 卡显存 × 利用率 − 上式 正数才可行
# 一行核对
P*b_w + k*L*C + A  ≤  卡显存 * 利用率目标(如 0.9)
# 不成立时,按"减并发 → 缩上下文 → 降精度 → 加卡"的顺序调

10. 四个常见误解

误解 实际
只算权重 权重只决定能不能装下;并发能力由 KV 决定,长上下文场景 KV 常超过权重
以为 KV Cache 是固定值 它随上下文与并发实时变化,峰值才是预算依据
忽略框架预留 CUDA context、workspace、碎片通常要占掉可观比例,按 100% 算必然 OOM
以为 OOM 一定是模型太大 多数是并发×长度算漏了,或 prefill 激活峰值没算,或碎片累积

11. 排错:五种显存相关的故障

现象 原因 处理
启动时 OOM,但模型明显装得下 加载峰值 + 框架预留没算 留出加载余量,或先量化再加载
稳态正常,长 prompt 一来就崩 prefill 激活峰值 限制单批 token 总数、开 chunked prefill
运行几小时后 OOM 碎片或 KV 未释放(断连未清理) 看块利用率,检查取消路径
显存够但并发上不去 准入按请求数而非 KV 字节 改成按剩余 KV 容量准入
多卡时单卡 OOM 负载不均或并行切分不均 检查并行配置与序列长度分布

12. 边界:这些估算什么时候不准

场景 说明
MoE 模型 总参数与激活参数不同,权重显存按总参数算,算力按激活算
极长上下文(数十万 token) KV 成为绝对主体,权重估算的意义下降
用了前缀缓存 复用部分的 KV 是共享的,按单请求独立估算会高估
托管 API 你看不到显存,这套方法只能用来反推成本与上限

13. 自测

  1. 显存的四份组成里,哪一份是常量、哪一份随流量变化?这个区别为什么重要?
  2. 写出权重的估算公式,并说明为什么 INT4 不等于精确的 0.5 倍。
  3. KV Cache 公式里的"2"代表什么?GQA 模型最容易在哪一项上算错?
  4. 为什么长上下文和高并发同时出现时,KV 需求是相乘而不是相加?
  5. OOM 最常发生在 prefill 还是 decode?对应的控制手段是什么?
  6. 装不下时的四种应对,按代价从小到大怎么排?为什么这么排?
S23-B02 · 显存构成:权重、KV Cache 与激活
https://www.clxhxhhr.top/posts/5996/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。