模型量化技术: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/BF16混合精度 INT8量化 按策略分类 动态量化 静态量化 训练方式 量化感知训练(QAT) 核心目标:在显存占用和模型精度之间找到最佳平衡点 FP16/BF16 → 显存减半 | INT8 → 显存减至1/4 | QAT → 精度损失最小化

量化这条路,我走了好几年。踩过坑,也收获过惊喜。但有一点我越来越确信:在部署场景下,量化不是「可选项」,而是「必选项」。尤其是大模型时代,不量化,你根本跑不动。

好了,今天就聊到这儿。记住一句话:量化不是玄学,是工程。多试、多调、多对比,你也能成为量化高手。

最后一个小建议:刚开始接触量化时,别追求一步到位。先从FP16混合精度开始,熟悉了再上INT8。QAT可以等你有了一定经验再尝试。循序渐进,稳扎稳打。

可可精品资料库