第3章 模型量化基础:FP32、FP16、INT8、INT4精度介绍与误差分析
量化,说白了就是给模型“减肥”。
一个训练好的神经网络,参数都是FP32的浮点数。每个数占4字节。一个7B的模型,光权重就要28GB。这在服务器上都够呛,更别说手机、摄像头这些端侧设备了。
我刚开始做端侧推理时,第一个想法就是:能不能把这些数用更少的位来表示?能。这就是量化。
3.1 四种精度的“身世”
先看看我们常用的几种数值精度。它们就像不同刻度的尺子——刻度越细,量得越准,但尺子也越重。
| 精度类型 | 位宽 | 表示范围 | 相对FP32存储节省 | 典型场景 |
|---|---|---|---|---|
| FP32 | 32位 | ~±3.4×10³⁸ | 1x(基准) | 训练、高精度推理 |
| FP16 | 16位 | ~±6.5×10⁴ | 2x | 训练加速、GPU推理 |
| INT8 | 8位 | [-128, 127] | 4x | 端侧推理、边缘计算 |
| INT4 | 4位 | [-8, 7] 或 [0, 15] | 8x | 极致压缩、特定硬件 |
FP32是“全精度”。每个数用1位符号、8位指数、23位尾数。它能表示非常大和非常小的数,精度也够。训练时几乎都用它。
FP16是“半精度”。指数和尾数都砍了一半。范围小了,精度也低了。但好处是显存占用减半,计算速度翻倍。我在训练大模型时,经常用混合精度——前向用FP16算,反向用FP32更新权重。又快又稳。
INT8和INT4就更有意思了。它们不是浮点数,是整数。没有指数,只有符号位和数值位。范围非常有限。但好处是——存储直接降到1/4和1/8,而且整数运算在CPU、DSP、NPU上快得飞起。
关键认知:量化不是简单的“砍位宽”,而是把浮点数映射到整数网格上。映射得好,精度损失极小;映射得不好,模型直接崩掉。
3.2 量化到底在做什么?
量化本质上是一个映射函数。把FP32的值域 [r_min, r_max] 映射到整数的值域 [q_min, q_max]。
最常见的映射是对称量化:
// 对称量化公式
scale = (r_max - r_min) / (q_max - q_min)
zero_point = 0 // 对称量化零点为0
// 量化:浮点 -> 整数
q = round(r / scale)
// 反量化:整数 -> 浮点
r = q * scale
举个例子。假设一组权重在 [-1.0, 1.0] 之间。要量化到INT8 [-128, 127]:
scale = (1.0 - (-1.0)) / (127 - (-128))
= 2.0 / 255
≈ 0.00784
// 浮点数 0.5 量化后:
q = round(0.5 / 0.00784) = round(63.7) = 64
// 反量化回来:
r = 64 * 0.00784 = 0.5018 // 有微小误差
你看,0.5变成了0.5018。这就是量化误差的源头——舍入误差。
我的经验:对称量化对权重效果不错,因为训练好的权重通常关于0对称。但对激活值(比如ReLU后的输出)就不太行了——它们全是正数。这时候用非对称量化(zero_point不为0)效果更好。
3.3 量化误差从哪来?
量化误差不是一种,是三种。我踩过的坑,基本都跟它们有关。
3.3.1 舍入误差
这是最直接的。浮点数映射到整数网格,必然有舍入。就像你用毫米尺量头发丝,总会有±0.5mm的误差。
单个舍入误差很小。但模型里动辄上亿个参数,误差累积起来就不可忽视了。尤其是某些层对精度特别敏感——比如BatchNorm的gamma参数,稍微偏一点,输出就飘了。
3.3.2 截断误差
这是最要命的。浮点数的范围比整数大得多。一旦某个值超出了量化范围,就会被“截断”到边界值。
举个例子。INT8的范围是[-128, 127]。如果某个权重是128.5,量化后变成127。反量化回来是127 * scale,跟128.5差了一大截。
我遇到过这种情况:一个模型量化后直接不工作了。查了半天,发现是某一层的权重有少量异常大值,把整个scale撑大了,导致大部分正常值被压缩到很小的区间,精度全丢了。
避坑指南:量化前一定要做“异常值裁剪”。把权重分布两端0.1%的极端值直接砍掉,反而能提升量化精度。我曾经因为不舍得裁剪,模型精度掉了5个点。裁剪后只掉了0.3个点。
3.3.3 跨层误差传播
这个最隐蔽。量化误差在层与层之间会传播、放大。
比如第一层量化后,输出有0.1%的误差。这个误差作为第二层的输入,经过计算后可能变成1%的误差。到了第五层,可能就变成10%了。
尤其是那些有“捷径连接”的结构(比如ResNet),量化误差会在不同路径上累积,最后在合并点爆发。
3.4 不同精度的“脾气”
FP16量化,我一般叫它“温柔量化”。范围够大,精度也还行。大部分模型直接转FP16,精度几乎不掉。我做过一个实验:ResNet-50从FP32转FP16,Top-1准确率只掉了0.02%。基本可以忽略。
INT8量化,是“主力量化”。存储省4倍,速度提升2-4倍。但需要做校准——拿一批数据跑一遍,统计激活值的分布,找到最优的scale和zero_point。我习惯用1000张左右的校准集,太少统计不准,太多浪费时间。
INT4量化,是“极限量化”。存储省8倍,但精度损失明显。一般只在特定场景用——比如语音唤醒、关键词检测这类对精度要求不高的任务。或者配合混合精度——敏感层用INT8,不敏感层用INT4。
我的建议:从FP16开始,精度达标就停。不达标再上INT8。INT4是最后的选择。别一上来就追求极致压缩,精度崩了再回头调,更浪费时间。
3.5 量化误差的“体检报告”
怎么知道量化后模型精度掉了多少?我一般看三个指标:
- 余弦相似度:量化前后,每一层输出的余弦相似度。低于0.99就要警惕了。
- 信噪比(SNR):量化噪声与原始信号的比值。SNR低于30dB,说明噪声太大了。
- 最终任务指标:比如分类准确率、检测mAP。这是最终标准。前两个指标再好看,任务指标掉了也是白搭。
我习惯在量化后先跑一遍余弦相似度。如果某层低于0.95,我会单独看这层的权重分布——是不是有异常值?是不是量化参数没选对?
3.6 一张图看懂量化流程
下面这张图,是我自己总结的量化决策流程。每次做量化前,我都会过一遍。
嗯,这张图基本概括了我的量化思路。先试FP16,不行再试INT8。INT8还不行,就做诊断——看看是哪层出了问题。是异常值?还是分布不均匀?对症下药。
3.7 量化后的“体检”怎么做?
我分享一个实用的小脚本。量化后,我会跑一遍每层的余弦相似度:
import torch
def check_quantization_quality(model_fp32, model_quant, calib_loader):
"""
检查量化后每层的余弦相似度
"""
similarities = {}
# 注册hook,获取每层输出
hooks_fp32 = []
hooks_quant = []
def hook_fn(name, storage):
def fn(module, input, output):
storage[name] = output.detach()
return fn
fp32_outputs = {}
quant_outputs = {}
for name, module in model_fp32.named_modules():
if isinstance(module, (torch.nn.Conv2d, torch.nn.Linear)):
hooks_fp32.append(
module.register_forward_hook(hook_fn(name, fp32_outputs))
)
for name, module in model_quant.named_modules():
if isinstance(module, (torch.nn.Conv2d, torch.nn.Linear)):
hooks_quant.append(
module.register_forward_hook(hook_fn(name, quant_outputs))
)
# 跑一遍校准数据
with torch.no_grad():
for batch in calib_loader:
model_fp32(batch)
model_quant(batch)
break # 只跑一个batch
# 计算余弦相似度
for name in fp32_outputs:
if name in quant_outputs:
a = fp32_outputs[name].flatten()
b = quant_outputs[name].flatten()
cos_sim = torch.nn.functional.cosine_similarity(
a.unsqueeze(0), b.unsqueeze(0)
)
similarities[name] = cos_sim.item()
# 清理hook
for h in hooks_fp32 + hooks_quant:
h.remove()
return similarities
这个脚本会输出每层的余弦相似度。我一般这样判断:
- ≥0.99:优秀,放心部署
- 0.95-0.99:还行,但建议关注
- <0.95:有问题,需要排查
小技巧:如果某层相似度特别低,试试把这层单独保留为FP32。混合精度往往能解决大部分问题。我有个项目,INT8量化后准确率掉了3%。最后只把第一层和最后一层保留为FP16,准确率就只掉了0.3%。
3.8 量化不是银弹
说了这么多,我得泼点冷水。量化不是万能的。
有些模型天生对量化不友好。比如MobileNet、EfficientNet这些轻量级模型,参数量本来就少,量化后精度损失更明显。反而是大模型(ResNet-50以上)量化效果更好——参数冗余多,砍掉一些精度也不怎么掉。
还有,量化对激活值的敏感度远高于权重。我见过一个案例:权重量化到INT8几乎不掉点,但激活值一量化,直接掉了5个点。原因就是激活值的分布太宽,量化网格不够细。
所以,我的建议是:量化前先做“敏感性分析”。逐层量化,看看哪层最敏感。敏感层保留高精度,不敏感层使劲压缩。这才是工程上的最优解。
最后提醒:量化后的模型一定要在目标硬件上测试。模拟量化(fake quantization)和实际硬件量化(hardware quantization)可能有差异。我吃过这个亏——PC上模拟跑得好好的,部署到手机上就崩了。原因就是硬件的INT8乘法器对溢出处理跟软件不一样。