2786 字
约 9 分钟
21
S23-X04 · 量化与显存预算

"这张卡 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. 自测

  1. 显存预算的四份账里,哪一份随并发线性增长、哪一份随长度线性增长?
  2. 为什么"参数量 × 0.5 字节"会低估 INT4 的实际占用?低估来自哪一项?
  3. PTQ 与 QAT 的取舍在什么条件下会反过来?
  4. 同一个 7B 模型的 AWQ 版与 GGUF Q4 版,速度差异主要来自哪里?
  5. KV 量化在 2K 上下文几乎无损,为什么不能据此推断 32K 也无损?
  6. 上下文长度翻倍时,并发上限按什么关系变化?依据哪个公式?
S23-X04 · 量化与显存预算
https://www.clxhxhhr.top/posts/5909/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。