2、模型量化基础:从FP32到INT8,量化原理与精度损失分析
好,咱们开始聊量化。说实话,量化这个话题在模型部署里太重要了。我刚开始做部署那会儿,模型跑在GPU上动不动就显存爆了,后来发现量化能直接把模型体积砍到原来的四分之一,推理速度还能翻倍。嗯,今天咱们就把量化的底裤扒干净。
2.1 为什么要量化?FP32有什么问题?
先看一个简单的数字。一个FP32的浮点数,占4个字节。一个INT8整数,只占1个字节。你想想看,一个模型几亿个参数,光存参数就得几个GB。我有个项目,模型1.2GB,手机端根本跑不动。量化到INT8后,直接降到300MB,速度还快了3倍。
FP32的问题说白了就两点:
- 存储开销大:每个参数32位,显存和内存都扛不住
- 计算速度慢:浮点运算比整数运算慢得多,尤其在移动端和边缘设备上
但量化不是免费的午餐。精度会掉,关键看你怎么平衡。我个人习惯是,先跑一遍FP32的baseline,再量化,对比精度差异。如果掉点超过1%,就得想办法了。
2.2 量化原理:映射与缩放
量化的核心思想很简单:用更少的比特数表示原来的数值范围。
比如一个FP32的权重范围是[-1.0, 1.0],我们要把它映射到INT8的范围[-128, 127]。怎么做?需要一个缩放因子(scale)和零点(zero point)。
公式长这样:
量化值 = round(原始值 / scale) + zero_point
反过来,反量化:
原始值 ≈ (量化值 - zero_point) * scale
其中scale = (原始最大值 - 原始最小值) / (量化最大值 - 量化最小值)。
举个例子:
原始范围: [-1.0, 1.0]
量化范围: [-128, 127]
scale = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 ≈ 0.00784
zero_point = round(-128 - (-1.0 / 0.00784)) ≈ 0
原始值 0.5 → 量化值 = round(0.5 / 0.00784) + 0 ≈ 64
量化值 64 → 原始值 ≈ (64 - 0) * 0.00784 ≈ 0.5018
你看,0.5变成了0.5018,这就是量化误差。误差不可避免,但我们可以控制。
关键点:量化不是简单的截断,而是带缩放和偏移的映射。scale和zero_point的选择直接影响精度。
2.3 对称量化 vs 非对称量化
这里有个选择:对称还是非对称?
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 对称量化 | zero_point = 0,量化范围对称 | 权重(通常分布对称) |
| 非对称量化 | zero_point ≠ 0,可偏移 | 激活值(分布可能偏斜) |
我在项目中遇到过一个问题:用对称量化量化激活值,结果精度掉了2%。后来换成非对称量化,精度只掉了0.3%。为什么?因为激活值经过ReLU后全是非负数,对称量化浪费了一半的表示范围。
我的建议:权重用对称量化,激活值用非对称量化。这是业界最常用的组合。
2.4 量化粒度:逐层 vs 逐通道
量化粒度也是个坑。你想想看,一个卷积层有多个输出通道,每个通道的数值范围可能差很多。如果整个层用一个scale,那范围大的通道会压缩小通道的精度。
- 逐层量化:整个层共用一个scale和zero_point。简单,但精度损失大。
- 逐通道量化:每个通道有自己的scale和zero_point。精度高,但计算复杂一点。
我曾经在一个图像分类模型上试过,逐层量化精度掉了1.5%,逐通道量化只掉了0.2%。所以,能逐通道就别逐层,除非你实在受限于硬件支持。
2.5 精度损失分析:到底丢了多少?
量化精度损失主要来自三个方面:
- 截断误差:超出量化范围的数值被截断
- 舍入误差:浮点到整数的四舍五入
- 累积误差:多层量化误差叠加
怎么评估?我一般用两个指标:
- 相对误差:|量化值 - 原始值| / |原始值|
- 余弦相似度:量化前后输出向量的相似度
举个例子,一个ResNet-50模型:
FP32 Top-1准确率: 76.1%
INT8逐层量化: 74.5% (掉1.6%)
INT8逐通道量化: 75.8% (掉0.3%)
你看,逐通道量化几乎无损。但要注意,小模型更容易掉精度。我做过一个MobileNet的量化,逐通道也掉了0.8%。
避坑指南:我曾经在量化一个BERT模型时,直接用了默认的逐层量化,结果下游任务F1分数掉了5个点。后来改成逐通道+混合精度(部分层保留FP16),才把损失控制在0.5%以内。所以,别盲目量化,先分析每层的敏感度。
2.6 量化感知训练(QAT) vs 训练后量化(PTQ)
这两个是主流方案,区别很大:
| 方法 | 流程 | 精度 | 成本 |
|---|---|---|---|
| PTQ | 训练完再量化,不需要重新训练 | 一般,可能掉1-2% | 低,几分钟搞定 |
| QAT | 训练过程中模拟量化,让模型适应 | 高,几乎不掉点 | 高,需要重新训练 |
我个人习惯是:先试PTQ。如果精度满足要求,那就直接用。如果掉点太多,再上QAT。毕竟QAT要重新训练,时间成本高。
QAT的原理是在前向传播时插入伪量化节点(fake quant),模拟量化误差,让模型学会补偿。代码大概长这样:
# 伪量化操作
def fake_quantize(x, scale, zero_point, qmin, qmax):
x_q = torch.round(x / scale + zero_point)
x_q = torch.clamp(x_q, qmin, qmax)
x_deq = (x_q - zero_point) * scale
return x_deq
注意,反向传播时用的是直通估计器(STE),把量化操作的梯度近似为1,这样才能训练。
2.7 实战建议:量化流程总结
好,说了这么多,给个我常用的量化流程:
- 跑FP32 baseline:记录准确率、延迟、显存占用
- PTQ快速尝试:用校准集计算scale和zero_point,评估精度
- 分析敏感层:逐层量化,找出掉点严重的层
- 混合精度:敏感层保留FP16或FP32,其他层用INT8
- 如果还不行:上QAT,重新训练几个epoch
- 验证:在测试集上对比FP32和量化后的结果
小技巧:校准集最好用训练集的子集,1000张图片左右就够了。别用测试集做校准,那叫数据泄露。
嗯,量化这块内容不少,但核心就这些。说白了,就是用精度换速度和存储,关键是怎么把精度损失降到最低。下一章咱们聊具体的量化工具和框架,比如TensorRT、ONNX Runtime这些,到时候手把手教你实操。