模型量化技术:FP16/BF16混合精度、INT8量化原理、量化感知训练、动态量化与静态量化
大家好,我是你们的老朋友。今天咱们聊聊模型量化。
说实话,我刚入行那会儿,对量化这事儿挺不屑的。总觉得「好好的FP32不用,搞什么量化?精度掉了怎么办?」后来被现实狠狠教育了一顿——模型太大,显存放不下,推理速度慢得像蜗牛。嗯,从那以后,我成了量化技术的忠实拥趸。
模型量化,说白了就是给模型「减肥」。把原本用32位浮点数(FP32)表示的权重和激活值,用更少的位数来表示。比如16位、8位,甚至4位。你想想看,参数数量不变,但每个参数占用的空间变小了,显存占用自然就降下来了。
FP16/BF16混合精度:最温柔的量化
先从最温和的量化方式说起——FP16和BF16混合精度。
FP16就是16位浮点数。相比FP32,它占用的显存直接减半。我有个项目,模型原本需要12GB显存,换成FP16后,6GB就搞定了。爽不爽?
但FP16有个问题——它的数值范围比较窄。FP32能表示大约10的38次方这么大的数,FP16只能表示到10的4次方左右。训练时梯度一爆炸,直接就溢出了。
这时候BF16就登场了。BF16的全称是「Brain Floating Point 16」,Google搞出来的。它和FP32有相同的指数位(8位),只是尾数位少了。这意味着什么呢?
- 数值范围:和FP32一样大,不怕梯度爆炸
- 精度:比FP32低,但比FP16高
- 显存占用:和FP16一样,减半
核心要点:训练时用混合精度——权重用FP16/BF16存储,但关键计算(如损失函数、梯度更新)用FP32。这样既省显存,又保精度。
我在项目中遇到过一个问题:用FP16训练一个BERT模型,loss死活不降。排查了半天,发现是某个层的激活值太小,FP16直接给截断成0了。换成BF16后,问题迎刃而解。
代码实现其实很简单,PyTorch自带AMP(Automatic Mixed Precision):
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for data, target in dataloader:
optimizer.zero_grad()
with autocast():
output = model(data)
loss = loss_fn(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
就这几行代码,显存占用直接砍半。我个人习惯是,只要显存吃紧,第一件事就是上混合精度。
INT8量化:真正的「瘦身」
FP16/BF16只是开胃菜,INT8才是主菜。
INT8用8位整数表示数值。相比FP32,显存占用只有原来的四分之一。你想想看,一个原本需要8GB显存的模型,量化到INT8后只需要2GB。这诱惑力,谁能顶得住?
但INT8的数值范围更小,只有-128到127。直接把FP32的权重截断到INT8,精度损失会非常大。所以我们需要一些技巧。
INT8量化的核心思想是:找到权重和激活值的分布范围,然后做一个线性映射。
公式很简单:
q = round(r / s) + z
其中:
r是原始的浮点数值q是量化后的整数值s是缩放因子(scale)z是零点偏移(zero point)
反量化就是:
r = (q - z) * s
说白了,就是找一个合适的尺子,把浮点数「量」成整数。尺子的刻度(s)越精细,量化误差越小。
我的经验:量化时最怕的就是「离群点」。如果权重分布中有几个特别大的值,整个缩放因子都会被它们带偏。我一般会先做一次「剪裁」,把极端值去掉,再量化。
量化感知训练:让模型学会「适应」量化
直接训练完再量化(训练后量化),精度损失往往比较大。那怎么办?让模型在训练时就「知道」自己将来要被量化,提前适应。
这就是量化感知训练(QAT,Quantization-Aware Training)。
QAT的原理其实不复杂:在训练过程中,在前向传播时模拟量化操作——把权重和激活值先量化成INT8,再反量化回FP32,然后继续计算。这样模型就学会了在「被量化」的情况下依然保持精度。
我做过一个对比实验:
| 方法 | 精度(准确率) | 显存占用 |
|---|---|---|
| FP32(基线) | 92.3% | 8GB |
| 训练后量化(INT8) | 89.1% | 2GB |
| 量化感知训练(INT8) | 91.8% | 2GB |
看到了吗?QAT比训练后量化高了将近3个点。虽然比FP32还是低了0.5%,但显存省了四分之三。这笔账,怎么算都划算。
PyTorch里实现QAT也很方便:
import torch.quantization as quant
# 在训练前插入量化/反量化节点
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
quant.prepare_qat(model, inplace=True)
# 正常训练
for epoch in range(num_epochs):
for data, target in dataloader:
optimizer.zero_grad()
output = model(data)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
# 训练完成后,转换为量化模型
quant.convert(model, inplace=True)
注意:QAT训练时,学习率要调小一些。我一般用原学习率的十分之一。因为量化操作会引入噪声,学习率太大容易震荡。
动态量化 vs 静态量化
最后聊聊两种量化策略:动态量化和静态量化。
动态量化:只在推理时对权重做量化,激活值还是FP32。每次推理时,动态计算激活值的量化参数。好处是实现简单,不需要校准数据集。坏处是推理速度提升有限,因为激活值没有量化。
静态量化:权重和激活值都量化成INT8。但激活值的量化参数需要提前通过「校准」得到——用一小批数据跑一遍模型,统计激活值的分布,然后确定缩放因子和零点偏移。
我个人的建议是:
- 如果追求快速部署,不想折腾校准数据集,用动态量化
- 如果追求极致性能,愿意花时间做校准,用静态量化
我曾经在一个OCR识别项目里,先用动态量化,推理速度提升了1.5倍。换成静态量化后,速度提升了3倍。代价是花了一个小时跑校准数据集。值不值?看你的场景。
静态量化的代码示例:
import torch.quantization as quant
# 准备模型
model.qconfig = quant.get_default_qconfig('fbgemm')
quant.prepare(model, inplace=True)
# 校准:用一小批数据跑一遍
with torch.no_grad():
for data in calibration_dataloader:
model(data)
# 转换为量化模型
quant.convert(model, inplace=True)
动态量化更简单:
quantized_model = quant.quantize_dynamic(
model, {torch.nn.Linear}, dtype=torch.qint8
)
一行代码搞定。
本章小结
量化技术,说白了就是「用精度换效率」。但好的量化策略,能把精度损失降到最低。
我给大家画了一张图,把今天讲的内容串起来:
量化这条路,我走了好几年。踩过坑,也收获过惊喜。但有一点我越来越确信:在部署场景下,量化不是「可选项」,而是「必选项」。尤其是大模型时代,不量化,你根本跑不动。
好了,今天就聊到这儿。记住一句话:量化不是玄学,是工程。多试、多调、多对比,你也能成为量化高手。
最后一个小建议:刚开始接触量化时,别追求一步到位。先从FP16混合精度开始,熟悉了再上INT8。QAT可以等你有了一定经验再尝试。循序渐进,稳扎稳打。