模型量化技术(上):FP32/FP16/BF16/INT8原理、量化感知训练、动态量化实战
聊到模型量化,我得先跟你掏心窝子说一句:这可能是你花最少成本,换来最大推理加速的手段。我见过太多团队,模型精度明明够用,却因为显存爆了、延迟高了,硬生生把项目拖垮。其实,很多时候只需要把精度从FP32降到FP16或者INT8,问题就迎刃而解了。
今天这一讲,咱们先把量化的“底裤”扒干净。从最基础的数值表示原理,到实战中怎么用代码做动态量化,我都会把踩过的坑和经验一并倒给你。
1. 为什么我们需要量化?
说白了,就是省钱省力。
你想想看,一个70B的大模型,用FP32存下来要280GB显存。就算用H100也得4张卡才能塞下。但如果你换成INT8呢?直接降到70GB,一张A100就搞定了。这还不算完,计算速度也能翻倍甚至翻三倍。
我在项目中遇到过最极端的情况:一个OCR模型在FP32下跑一次要800ms,客户嫌慢。我换成INT8动态量化后,直接降到120ms,精度只掉了0.3%。客户当场就拍板了。
- 显存占用降低:FP32 → INT8,理论减少75%
- 计算吞吐提升:低精度计算单元更多,带宽压力更小
- 功耗下降:移动端和边缘设备尤其明显
2. 四种精度的“底细”
咱们先搞清楚这四种格式到底长什么样。我习惯用“位宽 + 指数位 + 尾数位”这个框架去理解。
| 格式 | 总位宽 | 指数位 | 尾数位 | 表示范围 | 精度 |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | ~3.4e38 | 高(约7位有效数字) |
| FP16 | 16 | 5 | 10 | ~65504 | 中(约3位有效数字) |
| BF16 | 16 | 8 | 7 | ~3.4e38 | 低(约2位有效数字) |
| INT8 | 8 | 无 | 8 | -128~127 | 离散整数 |
2.1 FP32:老大哥,稳但重
FP32是深度学习默认的“通用货币”。它的动态范围大,精度也够。但问题是——太占地方了。我刚开始做模型部署时,总觉得FP32是“标准答案”,后来发现很多场景根本不需要这么高的精度。
2.2 FP16:速度快,但容易“炸”
FP16在NVIDIA的Tensor Core上跑得飞快。但有个致命伤:表示范围太小。最大值才65504。你想想看,如果模型里某个权重或者激活值超过这个数,直接就变成Inf了。我在训练一个Transformer时遇到过,loss突然变成NaN,查了半天才发现是FP16下梯度溢出了。
2.3 BF16:Google的“黑科技”
BF16是Google搞出来的。它保留了FP32的8位指数,所以动态范围跟FP32一样大。虽然尾数只有7位,精度差了点,但训练时基本够用。现在A100、H100都原生支持BF16,我个人非常推荐用它替代FP32做训练。
2.4 INT8:推理的“杀手锏”
INT8只有256个离散值。但它的计算速度极快,而且显存占用极低。为什么能用?因为神经网络的权重和激活值通常分布在一个很小的范围内。比如ReLU之后的值都在0附近。我们只需要把这个范围映射到-128~127之间就行。
3. 量化原理:从浮点到整数的“翻译”
量化的核心公式其实很简单:
Q = round( (R - zero_point) / scale )
其中:
- R:原始浮点值
- Q:量化后的整数值
- scale:缩放因子(浮点)
- zero_point:零点偏移(整数)
反量化就是反过来:
R ≈ Q * scale + zero_point
嗯,这里要注意:scale和zero_point怎么选,直接决定了量化精度。我见过有人直接用min-max映射,结果因为一个异常点,整个量化范围被拉得很大,精度掉得一塌糊涂。
4. 量化感知训练(QAT):让模型“学会”低精度
动态量化虽然方便,但精度损失有时候还是有点大。这时候就需要量化感知训练了。
说白了,就是在训练过程中模拟量化效果。我们在前向传播时插入伪量化节点(FakeQuant),把权重和激活值先量化再反量化。这样模型就能“看到”量化带来的误差,并主动去适应它。
import torch
import torch.nn as nn
# 一个简单的QAT示例
class QuantizedConv(nn.Module):
def __init__(self, in_ch, out_ch, kernel_size):
super().__init__()
self.conv = nn.Conv2d(in_ch, out_ch, kernel_size)
# 伪量化节点
self.quant = torch.quantization.FakeQuantize(
observer=torch.quantization.MinMaxObserver,
quant_min=-128, quant_max=127,
dtype=torch.qint8
)
def forward(self, x):
# 先量化再反量化,模拟INT8计算
x = self.quant(x)
x = self.conv(x)
x = self.quant(x)
return x
我在项目中用过一次QAT:一个语义分割模型,直接PTQ(训练后量化)掉点2.5个mIoU。用了QAT微调了3个epoch后,只掉了0.3个点。效果立竿见影。
4. 动态量化实战:一行代码搞定
如果你不想折腾训练,只想快速部署,那动态量化是你的不二之选。PyTorch里只需要一行代码:
import torch
# 加载一个预训练模型
model = torch.load('model_fp32.pth')
# 动态量化:只量化权重,激活值在推理时动态计算scale
quantized_model = torch.quantization.quantize_dynamic(
model,
{torch.nn.Linear, torch.nn.LSTM}, # 只量化这些层
dtype=torch.qint8
)
# 保存量化模型
torch.save(quantized_model.state_dict(), 'model_int8.pth')
为什么叫“动态”?因为激活值的scale和zero_point是在推理时实时计算的。每次前向传播,都会根据当前batch的数据重新算一遍。好处是精度高,坏处是计算量稍微大一点。
我建议你优先对全连接层和LSTM层做动态量化。卷积层用动态量化收益不大,因为卷积的计算瓶颈在访存,不在计算。
5. 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
6. 总结与建议
好了,这一讲的内容就到这里。我最后给你三个实操建议:
- 先试动态量化:成本最低,效果往往不错。如果精度不够,再考虑QAT。
- 优先量化Linear和LSTM:收益最大。卷积层建议用静态量化或者INT8校准。
- 一定要做精度验证:量化后的模型,必须用验证集跑一遍。我见过有人直接上线,结果精度掉到没法用。
torch.quantization.quantize_dynamic的qconfig_spec参数。你可以指定哪些子模块不量化,比如某些对精度特别敏感的层。
量化这条路,说白了就是用精度换效率。但只要你选对方法,精度损失完全可以控制在1%以内。下一讲,我们会深入静态量化和INT8校准的细节,到时候再聊。
想要更多资料,联系可可