模型量化怎么做的原理图解

文章导读
搜「模型量化怎么做的原理图解」的人,多数不是要一篇量化综述,而是手里已经有一份 fp16 或 fp32 权重,想知道那几个字节到底怎么被压掉、scale 和 zero_point 从哪来、自己动手时第一个脚本该写什么。原理图的核心只有一条链:统计值域 → 算 scale(和 zero_point)→ 映射到整数 → 反量化看误差。下面按实际动手顺序写,先做前置检查,再给最短可跑路径,最后列三个高频
📋 目录
  1. 一 模型量化之前先确认这三件事
  2. 二 模型量化:最短可用路径
  3. 三 模型量化:一个真实场景的完整做法
  4. 四 模型量化:跑不通先查这三处
A A

搜「模型量化怎么做的原理图解」的人,多数不是要一篇量化综述,而是手里已经有一份 fp16 或 fp32 权重,想知道那几个字节到底怎么被压掉、scale 和 zero_point 从哪来、自己动手时第一个脚本该写什么。原理图的核心只有一条链:统计值域 → 算 scale(和 zero_point)→ 映射到整数 → 反量化看误差。下面按实际动手顺序写,先做前置检查,再给最短可跑路径,最后列三个高频卡点。

量化的原理可以压缩成一张四步图:取张量的值域,用 max_abs/127(对称)或 (max-min)/255(非对称)算出 scale,把浮点值除以 scale 后四舍五入并夹到整数范围,推理时再乘回去。判断自己能不能动手,先确认 torch 版本、显存与权重读取权限、量化脚本的入口文件三件事,缺任一项都会卡在第一步。建议先在一层权重上跑通映射和误差打印,再整体套用;per-channel 粒度通常比 per-tensor 误差小,但要结合推理后端是否支持来定,不支持时会被静默降级。

模型量化之前先确认这三件事

版本、权限、入口位置,这三个前提任何一项没落实,后面的脚本都会以看不懂的方式失败。

  • 版本:量化 API 在 PyTorch 各版本间改过位置和签名。在目标机器上执行 python -c "import torch; print(torch.__version__)" 和 pip show bitsandbytes(若用 bnb 后端),把输出记下来。量化相关模块是否已废弃,去 PyTorch 官方文档的 Quantization 页面查当前版本对应的说明,不要凭记忆写 import。
  • 权限:模型权重从哪来必须先确认可读。本地目录用 ls -lh /path/to/model/ 看是否有 *.safetensors 或 pytorch_model.bin 且当前用户可读;从模型仓库拉取时执行 huggingface-cli whoami 确认 token 状态。GPU 侧用 nvidia-smi 看显存余量,量化过程本身也要驻留一份原始权重。
  • 入口位置:明确量化动作写在哪个文件里。自己写脚本就固定一个路径,例如 scripts/quantize.py,参数集中放 conf/quant_config.yaml;用现成工具就确认它的配置文件字段名,别把参数写在命令行又写在 yaml 里互相覆盖。

模型量化:最短可用路径

下面这条路径只做一件事:让你亲眼看到浮点值怎么变成整数、误差有多大。跑通后再决定用哪个推理后端。

  1. 第一步,取一层真实权重做样本。把 w = torch.randn(4096, 4096) * 0.02 换成从权重文件里读出的某一层,例如 model.layers.0.self_attn.q_proj.weight。做完应看到一个 shape 为 [4096, 4096]、dtype 为 float16 或 float32 的张量。
  2. 第二步,跑 per-tensor 对称量化,打印 scale 和最大误差。
import torch

w = torch.randn(4096, 4096) * 0.02   # 替换为真实权重

max_abs = w.abs().max().item()
scale = max_abs / 127
q = torch.clamp(torch.round(w / scale), -127, 127).to(torch.int8)
dq = q.float() * scale

print("scale =", scale)
print("q range =", q.min().item(), q.max().item())
print("max abs err =", (w - dq).abs().max().item())

做完应看到 q range 落在 -127 到 127 之间,max abs err 大致在 scale/2 的量级。如果 max abs err 明显大于 scale,说明某处 dtype 或夹取范围写错了。

  1. 第三步,换成 per-channel。把 scale 从标量改成按输出通道一行一个:
scale = w.abs().amax(dim=1, keepdim=True) / 127
q = torch.clamp(torch.round(w / scale), -127, 127).to(torch.int8)
dq = q.float() * scale
print("per-channel max abs err =", (w - dq).abs().max().item())

做完应看到误差通常比 per-tensor 小,因为每行独立取值域。

  1. 第四步,非对称量化的公式对照。如果后端要求 uint8,公式改为 scale = (max - min) / 255、zero_point = round(-min / scale)、q = clamp(round(w / scale) + zero_point, 0, 255)。做完检查 zero_point 是否落在 0 到 255 内,越界说明值域统计有问题。
  2. 第五步,落盘并记录参数。把 bits、granularity、symmetric、scale 的存放位置写进 conf/quant_config.yaml。做完应能用同一份配置复现出相同的 q 张量。

模型量化:一个真实场景的完整做法

场景:把一个 7B 参数级别的开源语言模型权重(约 14GB fp16,约 200 个权重张量,主要是 Linear 层)量化到 int8,用于本地推理,显存只有 24GB。

第一步先在 scripts/inspect.py 里遍历 named_parameters(),只统计名字含 proj 或 mlp 的 Linear 权重,打印每个张量的 abs().max()。这一步是为了找离群值——如果某一层 max 比同类型层大一个数量级,per-tensor 量化会把它附近的值全部压扁。

第二步在 conf/quant_config.yaml 里定死参数,实际取值示例:

模型量化怎么做的原理图解
quant:
  bits: 8
  granularity: per_channel
  symmetric: true
  skip_layers:
    - lm_head
    - embed_tokens
  calib_samples: 128

跳过 lm_head 和 embed_tokens 是常见做法,这两处对输出分布敏感,量化后容易出现重复或乱码。calib_samples 只在需要激活量化的方案里生效,纯权重量化可以忽略;具体字段名以所用工具配置文件里的实际命名为主,不必照抄。

第三步执行 python scripts/quantize.py --config conf/quant_config.yaml --out ./model-int8。做完检查两件事:ls -lh ./model-int8 的文件总量应明显小于原始权重目录;用同一段 prompt 分别喂给原模型和量化模型,比较前几十个 token 是否基本一致,出现整段复读就要回到离群值那一步。

模型量化:跑不通先查这三处

卡点一:显存不够。报错原文通常是 RuntimeError: CUDA out of memory. Tried to allocate ...。先看 nvidia-smi 里是否有别的进程占着显存;再确认量化脚本是不是把整份权重和反量化副本同时留在 GPU 上——把统计值域和映射放到 .cpu() 上做通常能绕开。这不是优化,只是让流程跑得下去。

卡点二:API 找不到或签名不符。报错原文形如 AttributeError: module 'torch.quantization' has no attribute 'quantize_dynamic' 或 ImportError: cannot import name ...。先回到 python -c "import torch; print(torch.__version__)" 看版本,再到官方 Quantization 文档对应版本页面确认模块路径,不要靠改 import 试错。

卡点三:量化后输出重复、乱码或直接崩。页面提示往往没有,只有结果异常。按顺序查三处:skip_layers 是否真的生效(打印量化前后 named_modules() 对比);granularity 是否被推理后端静默降级为 per-tensor(看它加载时打印的配置,或读回保存的 scale 张量形状是标量还是一维);值域统计是否用的是校准样本而非全量数据,样本太少时 max 会被个别离群值主导。

三处都排除后仍不通,把问题缩小到单层:只量化一层、其余保持原精度,逐层加回去,能定位到具体哪类层不适合 int8。这一步比整体重跑快得多。