量化粒度与策略:Per-tensor、Per-channel、Per-group量化;对称量化与非对称量化

聊到量化,很多人第一反应就是「把 FP16 变成 INT8」。但真正上手做的时候你会发现,怎么量化量化成什么格式更关键。量化粒度选错了,模型精度可能直接崩掉。今天我就把这块掰开揉碎了讲清楚。

一、量化粒度:从粗到细的三个层次

量化粒度,说白了就是多少个参数共享同一个缩放因子。粒度越粗,计算越简单,但精度损失越大;粒度越细,精度保留越好,但计算开销也上去了。这是个典型的 trade-off。

1. Per-tensor 量化(最粗粒度)

整个张量只用一个 scale 和 zero_point。比如一个 4x4 的权重矩阵,所有 16 个元素都用同一个缩放因子。

# 伪代码示意
scale = max(abs(tensor)) / 127.0
quantized_tensor = round(tensor / scale)

我刚开始做量化时,第一个版本就用的 per-tensor。当时觉得「一个 scale 搞定多省事」。结果跑起来发现,某些层的输出误差大得离谱。后来一查,原来是权重分布不均匀——大部分值集中在 0 附近,但有几个 outlier 把 scale 撑得很大,导致小值被严重压缩。

避坑指南: 我曾经在一个 7B 模型上试过全 per-tensor 量化,精度掉了 3 个点。后来换成 per-channel,只掉了 0.5 个点。如果你的模型有 outlier,per-tensor 要慎用。

2. Per-channel 量化(中等粒度)

每个输出通道独立计算 scale。对于权重矩阵来说,就是每一行(或每一列)都有自己的缩放因子。

# 假设权重 shape 为 [out_channels, in_channels]
for c in range(out_channels):
    scale[c] = max(abs(weight[c, :])) / 127.0
    quantized_weight[c, :] = round(weight[c, :] / scale[c])

这是目前工业界最常用的方案。为什么?因为卷积层和线性层的权重,不同通道的分布差异往往很大。你想想看,一个卷积核负责检测边缘,另一个负责检测纹理,它们的数值范围能一样吗?

我个人习惯是:权重用 per-channel,激活用 per-tensor。激活的分布相对稳定,per-tensor 就够了。当然,如果你追求极致精度,激活也可以做 per-channel,但计算复杂度会上升。

3. Per-group 量化(最细粒度)

把通道再分成若干组,每组独立量化。比如 128 个通道一组,或者 32 个一组。

# group_size = 128
num_groups = in_channels // group_size
for g in range(num_groups):
    group_data = weight[:, g*group_size : (g+1)*group_size]
    scale[g] = max(abs(group_data)) / 127.0
    quantized_group = round(group_data / scale[g])

这个方案在低比特量化(比如 4-bit)时特别有用。我记得有一次做 4-bit 量化,per-channel 精度已经不行了,换成 group_size=32 的 per-group,硬是把精度拉回来了。

经验之谈: group_size 一般取 32、64、128。太小了 scale 数量太多,存储开销大;太大了又退化成 per-channel。我通常从 128 开始试,不行再往下调。

二、对称量化 vs 非对称量化

这两个概念其实很简单,但很多人容易搞混。我直接上公式:

类型 公式 zero_point 适用场景
对称量化 q = round(x / scale) 固定为 0 权重(通常对称分布)
非对称量化 q = round(x / scale) + zero_point 可偏移 激活(ReLU 后全正数)

对称量化:简单粗暴

输入范围是 [-max, max],映射到 [-127, 127](INT8)。zero_point 永远是 0,计算时少一次加减法。

为什么权重适合对称量化?因为训练好的权重,正负值分布通常是对称的。你想想看,一个卷积核的权重,正数和负数的最大值差不多。这时候用对称量化,不会浪费量化区间。

非对称量化:灵活适配

输入范围是 [min, max],映射到 [0, 255](UINT8)。zero_point 可以偏移,用来适配非对称分布。

激活函数比如 ReLU,输出全是非负数。这时候用对称量化,一半的量化区间(负数部分)就浪费了。非对称量化可以把整个 [0, max] 映射到 [0, 255],精度更高。

核心要点: 对称量化计算快,适合权重;非对称量化精度高,适合激活。我见过有人反过来用,结果精度掉了不少。

三、知识体系总览

下面这张图把量化粒度和策略的关系梳理清楚了。你可以把它当作一个决策树:

量化策略选择 量化粒度 量化策略 Per-tensor Per-channel Per-group 对称量化 非对称量化 权重:对称 权重:对称 低比特时推荐 权重首选 激活首选 推荐组合:权重 per-channel + 对称量化,激活 per-tensor + 非对称量化 低比特场景(4-bit)考虑 per-group + 对称量化

四、实际项目中的选择策略

说了这么多理论,到底怎么选?我直接给结论:

  1. 8-bit 量化:权重 per-channel + 对称,激活 per-tensor + 非对称。这是最稳的组合,90% 的场景都适用。
  2. 4-bit 量化:权重 per-group(group_size=32 或 64)+ 对称,激活 per-tensor + 非对称。精度不够时,把 group_size 调小。
  3. 混合精度:敏感层用高精度(比如 8-bit),不敏感层用低精度(4-bit)。我有个项目就是这么干的,精度只掉了 0.1%,但推理速度提升了 40%。
一个小技巧: 不确定选哪种粒度时,先跑一个 calibration 数据集,统计每层的权重和激活分布。如果某层的分布特别宽,就给它更细的粒度。我一般用这个方法快速定位问题层。

嗯,量化粒度和策略这块就讲到这里。记住一句话:没有最好的方案,只有最适合你模型的方案。多试几次,你就能找到那个平衡点。


公众号:蓝海资料掘金营,微信deep3321