3、权重量化实战:对称量化与非对称量化,Per-tensor与Per-channel量化
好,咱们直接进入正题。权重量化,说白了就是把模型里那些浮点数权重,用更小的整数来表示。你想想看,一个FP32的权重占4个字节,换成INT8就只要1个字节,内存直接砍到四分之一。这买卖划算不?但问题来了——怎么砍才能不伤筋动骨?
我个人习惯把量化分成两个维度来理解:一个是量化范围(对称 vs 非对称),另一个是量化粒度(Per-tensor vs Per-channel)。咱们一个一个说。
3.1 对称量化 vs 非对称量化
先讲对称量化。这个名字很直白——量化后的零点(zero point)固定在0。什么意思呢?就是正负数的范围是对称的。比如INT8,范围是[-128, 127],零点就在0上。
公式长这样:
// 对称量化
scale = max(|x_max|, |x_min|) / 127
quantized_value = round(x / scale)
我在项目中遇到过一个问题:有些模型的权重分布是偏的,比如ReLU之后的权重全是非负的。这时候用对称量化,有一半的量化范围就浪费了。你想想看,明明数值都在0到正数这边,结果你还给负数那边留了128个格子,这不是浪费是什么?
所以就有了非对称量化。它的零点可以偏移,公式变成:
// 非对称量化
scale = (x_max - x_min) / 255
zero_point = round(-x_min / scale)
quantized_value = round(x / scale) + zero_point
说白了,非对称量化就是把整个数值范围都利用起来。我做过一个MobileNet的量化实验,权重分布严重偏右,用对称量化精度掉了2.3%,换成非对称量化只掉了0.7%。差距很明显。
核心区别总结:
- 对称量化:零点固定为0,计算简单,适合权重分布对称的场景
- 非对称量化:零点可偏移,利用率高,适合ReLU后的激活值
我的建议:权重一般用对称量化就够了,因为预训练模型的权重分布通常接近正态分布。激活值建议用非对称量化,尤其是经过ReLU之后。
3.2 Per-tensor vs Per-channel 量化
好,接下来是量化粒度的问题。这玩意儿坑很多,我踩过不少。
Per-tensor量化,就是整个张量共享一个scale和zero point。比如一个卷积层的权重形状是[64, 3, 3, 3],那这64个输出通道、3个输入通道、3x3的核,全部用同一个量化参数。
这样做的好处是啥?简单!参数少,计算快。但问题也很明显——不同通道的数值范围可能差很多。我记得有一次量化一个深度可分离卷积,第一个通道的权重范围是[-0.1, 0.1],最后一个通道是[-2.0, 2.0]。用Per-tensor量化,scale被最大范围的那个通道拉大了,小范围的通道几乎被压成了0。
结果呢?精度直接崩了。
Per-channel量化就是每个通道独立算scale。还是那个[64, 3, 3, 3]的例子,现在每个输出通道都有自己的scale和zero point。代价是多了64倍的量化参数,但精度保住了。
| 量化粒度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Per-tensor | 计算简单,参数少 | 通道间差异大时精度损失严重 | 小模型、对精度不敏感的场景 |
| Per-channel | 精度高,适应性强 | 计算稍复杂,参数多 | 大模型、精度敏感场景 |
我曾经踩过的坑:有一次量化一个BERT模型,我图省事用了Per-tensor对称量化。结果下游任务F1值掉了4个点。排查了半天,发现是某些attention头的权重范围差异太大。换成Per-channel后,F1只掉了0.3个点。嗯,从那以后我再也不敢在Transformer上偷懒用Per-tensor了。
3.3 实战:怎么选?
说了这么多理论,咱们来点实际的。我一般按这个流程来:
- 先看权重分布:画个直方图,看看是不是对称的。对称就用对称量化,不对称就用非对称。
- 再看通道差异:算一下每个通道的max和min,如果最大/最小比值超过10倍,果断上Per-channel。
- 最后做消融实验:拿验证集跑一遍,对比不同配置的精度损失。
代码示例(PyTorch风格):
# 对称量化 + Per-tensor
def quantize_symmetric_per_tensor(tensor):
max_val = tensor.abs().max()
scale = max_val / 127.0
quantized = torch.round(tensor / scale).clamp(-128, 127)
return quantized, scale
# 非对称量化 + Per-channel
def quantize_asymmetric_per_channel(tensor, dim=0):
# tensor shape: [out_channels, in_channels, k_h, k_w]
min_val = tensor.amin(dim=(1,2,3), keepdim=True)
max_val = tensor.amax(dim=(1,2,3), keepdim=True)
scale = (max_val - min_val) / 255.0
zero_point = torch.round(-min_val / scale)
quantized = torch.round(tensor / scale) + zero_point
quantized = quantized.clamp(0, 255)
return quantized, scale, zero_point
小技巧:实际部署时,很多硬件对Per-channel量化的支持有限。比如某些NPU只支持Per-tensor的对称量化。这时候怎么办?我的做法是:先用Per-channel量化训练一个校准后的模型,然后转成Per-tensor时做一次通道对齐——把每个通道的scale取平均或者取最大值,牺牲一点精度换取硬件兼容性。
最后说一句,量化没有银弹。你想想看,每个模型、每个任务、每个硬件都不一样。我见过有人把ResNet-50量化到INT8精度几乎不掉,也见过有人把EfficientNet量化后直接没法用。关键还是得多试、多调、多踩坑。
嗯,这一节就到这儿。下一节咱们聊聊激活值量化的那些破事——比权重量化坑多多了。