"这张卡 24G,7B 模型够不够跑?"——这个问题没有单一答案,因为显存不是被模型权重独占的。权重、KV Cache、激活与临时缓冲、框架自身开销四份账各自独立增长,而其中只有第一份能被量化直接压缩。
预算算错的典型后果不是启动即崩,而是"能启动、跑几个请求就 OOM",或者更隐蔽的"没 OOM 但并发被悄悄压到 1"。下面把四份账逐项拆开,给出可代入的公式与假设,剩下的数字按你自己的模型结构核对。
1. 显存预算的四份账
| 构成 | 决定因素 | 是否随并发增长 | 压缩手段 |
|---|---|---|---|
| 权重 | 参数量 × 每参字节 | 否 | 量化、蒸馏、换小模型 |
| KV Cache | 层数 × KV 头 × 头维 × 长度 | 是,乘法增长 | KV 量化、降上下文、限并发 |
| 激活与临时缓冲 | batch 内最大 token 数、prefill 长度 | 是,随单步规模 | 限 max_num_batched_tokens |
| 框架与运行时 | CUDA context、通信缓冲、日志 | 基本固定 | 预留比例,别顶满 |
总显存 ≈ 权重 + KV(长度, 并发) + 激活(B) + 固定开销(约 1–2 GiB 量级,按后端核对)
固定开销这一项差异很大:CUDA context、NCCL 缓冲、PyTorch 缓存分配器都会占,多卡时每卡都要一份。gpu_memory_utilization=0.9 的意思是"引擎只用其中 90%",不是"总共只占 90%"。
2. 权重的量化换算
| 精度 | 每参字节 | 说明 |
|---|---|---|
| FP32 | 4 | 训练用,推理基本不用 |
| FP16 / BF16 | 2 | 推理基线,数值范围 BF16 更稳 |
| FP8 | 1 | 需要硬件支持(Hopper 及以后) |
| INT8 | 1 | 需 per-channel/per-group scale |
| INT4 | 约 0.5 | 实际更大,见下 |
"INT4 就是 0.5 字节/参"只对理想情况成立。真实存储还要带 scale(有时带 zero-point):
每参字节 ≈ 0.5 + scale_bytes / group_size (+ zero_bytes / group_size)
group_size=128、scale 为 FP16 时,额外开销约 2/128 ≈ 0.016 字节/参,实际约 0.52;某些格式为零点再付一份,或采用不在 128 整除的混合块。按参数量 × 0.5 直接估,会系统性低估 3%–8%,模型越大这笔差额越明显。
def weight_gib(params_b, bits_per_weight):
return params_b * 1e9 * (bits_per_weight / 8) / 2**30
for p in (7, 13, 70):
print(p, "B FP16:", round(weight_gib(p, 16), 1),
" INT8:", round(weight_gib(p, 8), 1),
" INT4(含开销 4.3bit):", round(weight_gib(p, 4.3), 1), "GiB")
表内数字由公式推出,只含权重,不含 KV 与激活。
| 参数量 | FP16 | INT8 | INT4(含 scale 开销) |
|---|---|---|---|
| 7B | 13.0 GiB | 6.5 GiB | 约 3.5 GiB |
| 13B | 24.2 GiB | 12.1 GiB | 约 6.5 GiB |
| 70B | 130.4 GiB | 65.2 GiB | 约 35.1 GiB |
注意 70B 那一行:即使 INT4 也要三十五 GiB 上下,单张 24G 卡放不下,必须切分或多卡。
3. 两种量化时机
| 路线 | 做法 | 优点 | 代价 |
|---|---|---|---|
| PTQ(训练后量化) | 用校准集或少量样本拟合法定 scale | 无需训练资源,几十分钟到几小时 | 精度损失不可控,需评测兜底 |
| QAT(量化感知训练) | 训练中插入伪量化节点 | 精度更接近原模型 | 要训练管线、算力、数据 |
工程上绝大多数选 PTQ:模型不是自己训的,也拿不到训练管线。PTQ 里再分两类——权重仅量化(weight-only,激活仍高精度,省显存不掉太多速度)与权重激活都量化(W8A8/FP8,省算力但更敏感)。推理显存瓶颈场景,weight-only 通常已经够用。
4. 主流方法的取舍
| 方法 | 量化对象 | 典型落地 | 硬件/框架约束 |
|---|---|---|---|
| GPTQ | 权重 INT4,逐层误差补偿 | vLLM、TensorRT-LLM、ExLlama | 需要校准数据;部分 kernel 依赖特定架构 |
| AWQ | 权重 INT4,按激活幅度保护显著通道 | vLLM、TRT-LLM、LMDeploy | 对指令模型友好,长尾保留较好 |
| GGUF k-quant | 混合位宽块量化(Q4_K_M 等) | llama.cpp、Ollama | CPU/混合卸载友好,GPU 生态较窄 |
| bitsandbytes NF4 | 4-bit 正态分布分位量化 | Transformers 直接 load_in_4bit |
方便但吞吐不如专用 kernel |
# 三种加载方式,注意它们不通用、不能互转
from transformers import AutoModelForCausalLM
m = AutoModelForCausalLM.from_pretrained("MODEL", load_in_4bit=True,
bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype="bfloat16")
# vLLM 侧:GPTQ / AWQ 走量化名后缀或显式参数
vllm serve MODEL-AWQ --quantization awq --dtype auto --max-model-len 4096
vllm serve MODEL-GPTQ --quantization gptq --dtype float16
# GGUF:位宽写在文件名里,按当前 llama.cpp / Ollama 版本核对可用档位
ollama run MODEL:q4_k_m
选型判断很短:追求 GPU 吞吐选 AWQ 或 GPTQ,追求部署简单与 CPU 回退选 GGUF,追求"先跑起来"选 NF4。 同一个模型的不同量化版本,性能差距常常来自 kernel 实现而非模型能力。
5. 量化精度的风险面
| 风险面 | 表现 | 缓解 |
|---|---|---|
| 长尾知识 | 罕见实体、冷门事实答错或编造 | 保留高精度副本做校验,关键业务不量化 |
| 数值推理 | 多步算术、单位换算开始出错 | 加工具调用/计算器,别让模型硬算 |
| 结构化输出 | JSON 括号、字段名偶发破损 | 约束解码(grammar/JSON schema)兜底 |
| 长上下文 | 远处信息召回变差 | 单独评测长上下文任务,别只看短问答 |
| 多语言 | 小语种质量塌陷更明显 | 按语种分层评测 |
共同点是:平均分掉得不多,尾部掉得很狠。所以量化后的验收不能只看一个总分,要按任务类型分组看,尤其是那些"错了就得重来"的路径。
6. 混合精度:敏感层保留高精度
不是所有层都同样怕量化。常见做法是 embedding、lm_head、第一层与最后几层、MoE 的 router 保留 BF16,其余 INT4。
{
"quant_method": "awq",
"bits": 4,
"group_size": 128,
"modules_to_not_convert": ["lm_head", "model.embed_tokens",
"model.layers.0", "model.layers.31"]
}
代价是显存预算里要单独给这些层留一份,估算时把这部分按 2 字节/参算,不能一律 0.5。
7. KV Cache 量化
KV Cache 可以单独量化到 FP8/INT8,收益是 KV 占用减半,进而并发翻倍或上下文翻倍。
| 方案 | 显存收益 | 质量影响 | 备注 |
|---|---|---|---|
| BF16 基线 | — | — | 默认 |
| FP8 KV | 约 2× | 长上下文召回可能下降 | 需硬件支持,vLLM 有 --kv-cache-dtype fp8 |
| INT8 KV | 约 2× | 类似,标定更敏感 | 逐头 scale 更稳 |
vllm serve MODEL --kv-cache-dtype fp8 --max-model-len 8192 --max-num-seqs 32
KV 量化的风险与上下文长度正相关:短上下文几乎无损,32K 以上开始出现"中间信息丢失"。上线前必须在目标长度上测,不能拿 2K 的结果推 32K。
8. 显存不够的四条出路
| 出路 | 省什么 | 延迟代价 | 适用 |
|---|---|---|---|
| 换更小模型 | 权重 + KV 同时降 | 能力下降,延迟可能更好 | 任务简单、可接受质量损失 |
| 量化 | 主要省权重 | 解码可能更快,prefill 略慢 | 首选,成本最低 |
| 减并发 | 省 KV 与激活 | 排队变长,吞吐降 | 保 p99 的应急手段 |
| 卸载到 CPU/磁盘 | 省权重(层卸载) | 每 token 走 PCIe,显著变慢 | 只能演示或低 QPS 场景 |
层卸载的代价量级:卸载的层每一步都要把权重从内存搬到显存,PCIe 带宽比 HBM 低一个量级以上,交互式场景基本不可接受。它是"能跑"方案,不是"能服务"方案。
9. 批量与并发的反比
给定卡容量 V,权重占 W,每请求 KV 占 k(S):
可用 KV 空间 = V × util − W − 激活 − 固定开销
理论并发上限 ≈ 可用 KV 空间 / k(S)
k(S) 与上下文长度成正比,所以并发与上下文长度成反比。把 max-model-len 翻倍,并发上限减半,这是硬约束,任何调度技巧都改不了。
def max_concurrency(v_gib, w_gib, kv_per_token_bytes, seq_len, util=0.9, overhead=1.5):
free = v_gib * util - w_gib - overhead
per_req = kv_per_token_bytes * seq_len / 2**30
return max(0, int(free // per_req)), round(free, 2)
print(max_concurrency(24, 3.5, 131072, 4096)) # 24G 卡、INT4 7B、4K 上下文
把 max_num_seqs 定在上限的六到七成,剩下的余量留给激活波动与抢占缓冲。
10. 排错:五个高频误判
| 现象 | 根因 | 处理 |
|---|---|---|
| 按参数量 × 字节估完,部署仍 OOM | 漏了 KV、激活、框架开销 | 用第 9 节公式重算,留 10% 余量 |
| 短请求正常,长请求崩 | KV 随长度增长,预算按短上下文算的 | 按 max-model-len 重算并发 |
| 量化后评测分数没掉,线上却答错 | 评测集太简单,没覆盖长尾与结构化输出 | 分组评测 + 约束解码 |
| 换量化格式后"模型变笨了" | kernel/实现差异,或 chat template 不匹配 | 先核对模板与参数,再下结论 |
| 单卡能跑,多卡反而更慢 | TP 通信开销 + 每卡固定开销叠加 | 小模型优先单卡,别为并行而并行 |
11. 边界:量化解决不了的情况
| 场景 | 量化是否有效 | 说明 |
|---|---|---|
| 并发不足导致排队 | 部分有效 | 省下的显存能换并发,但受带宽上限约束 |
| 长上下文 OOM | 部分有效 | KV 量化省一半,长度翻倍仍会撞墙 |
| 输出格式不稳定 | 无效 | 该用约束解码,不是调精度 |
| 首 token 太慢 | 基本无效 | prefill 是算力问题,量化收益有限 |
| 多卡通信瓶颈 | 无效 | 需要换并行策略或拓扑 |
12. 自测
- 显存预算的四份账里,哪一份随并发线性增长、哪一份随长度线性增长?
- 为什么"参数量 × 0.5 字节"会低估 INT4 的实际占用?低估来自哪一项?
- PTQ 与 QAT 的取舍在什么条件下会反过来?
- 同一个 7B 模型的 AWQ 版与 GGUF Q4 版,速度差异主要来自哪里?
- KV 量化在 2K 上下文几乎无损,为什么不能据此推断 32K 也无损?
- 上下文长度翻倍时,并发上限按什么关系变化?依据哪个公式?