4、模型量化原理(上):什么是量化?对称量化 vs 非对称量化,量化参数(scale, zero_point)的计算

好,咱们今天聊聊量化。说实话,很多搞模型部署的朋友一听到「量化」两个字,第一反应就是「精度掉得厉害」。其实不然。我在早期做移动端推理引擎的时候,也踩过这个坑——一开始不敢碰量化,总觉得 float32 才是王道。后来被逼着在嵌入式设备上跑大模型,没办法,硬着头皮上了 int8。结果你猜怎么着?精度几乎没掉,推理速度翻了三倍。

所以今天这节,咱们先把量化的底裤扒干净。说白了,量化就是把模型里的 float32 参数,用更小的数据类型(比如 int8)来表示。为什么要这么做?因为显存带宽和计算单元都更喜欢小数据。你想想看,一张显卡一次能吞吐的 int8 数据量,是 float32 的四倍。这速度不就上来了吗?

核心思想:用更少的比特数,去近似表示原来的浮点数。代价是损失一点点精度,换来巨大的速度提升和内存节省。

4.1 什么是量化?一个简单的例子

假设你有一组浮点数:[0.5, 1.2, 3.8, -2.1, -5.6]。你想把它们映射到 int8 的范围,也就是 [-128, 127]。怎么做?

嗯,这里要注意:量化本质上是一个「映射」过程。我们把一个连续的浮点区间,映射到一个离散的整数区间。就像把一张高清照片压缩成 256 色 GIF 图一样——颜色少了,但轮廓还在。

我个人习惯把量化理解成「缩放+平移」。浮点数世界里的一个值,经过缩放(scale)和平移(zero_point),就变成了整数世界里的一个值。反过来,整数也能还原成浮点数,只不过会有一些误差。

我的经验:量化误差是不可避免的,但关键是要控制误差的分布。我曾经在一个语音识别模型上试过,如果量化参数选得不好,某些敏感层的误差会被放大,导致整个模型崩掉。所以量化参数的选取,是门手艺活。

4.2 对称量化 vs 非对称量化

量化的方式主要有两种:对称量化和非对称量化。这俩的区别,说白了就是「零点要不要对齐」。

4.2.1 对称量化

对称量化,就是让浮点数的 0 映射到整数的 0。也就是说,zero_point = 0。这种情况下,我们只需要一个 scale 参数就够了。

公式很简单:

量化:q = round(x / scale)
反量化:x = q * scale

其中 scale 的计算方式:

scale = max(|x_max|, |x_min|) / (Q_max - Q_min) / 2

举个例子,假设浮点数范围是 [-5.6, 3.8],那么 max(|x_max|, |x_min|) = 5.6。int8 的量化范围是 [-128, 127],所以 Q_max - Q_min = 255。那么:

scale = 5.6 / (255 / 2) = 5.6 / 127.5 ≈ 0.0439

你看,对称量化只用了绝对值最大的那个边界。这意味着,如果数据分布不对称(比如大部分是正数,只有少量负数),那么量化精度就会浪费在那些不存在的负数区间上。

避坑指南:我曾经在一个图像分类模型上用了对称量化,结果发现精度掉了 2 个点。排查了半天,发现是激活值的分布严重偏正,对称量化把一半的量化区间浪费在了负数上。后来换成非对称量化,精度立马回来了。

4.2.2 非对称量化

非对称量化就灵活多了。它允许浮点数的 0 映射到任意整数,也就是 zero_point 可以不为 0。这样,我们可以把整个量化区间都用在有效数据上。

公式:

量化:q = round(x / scale) + zero_point
反量化:x = (q - zero_point) * scale

scale 和 zero_point 的计算:

scale = (x_max - x_min) / (Q_max - Q_min)
zero_point = round(Q_min - x_min / scale)

还是刚才那个例子,浮点数范围 [-5.6, 3.8]

scale = (3.8 - (-5.6)) / 255 = 9.4 / 255 ≈ 0.0369
zero_point = round(-128 - (-5.6) / 0.0369) = round(-128 + 151.8) = round(23.8) = 24

你看,非对称量化把整个 [-5.6, 3.8] 区间都映射到了 [-128, 127] 上,没有浪费。这就是它的优势。

特性 对称量化 非对称量化
zero_point 固定为 0 可调节
计算复杂度 低(少一次减法) 稍高
适用场景 权重(通常对称分布) 激活值(分布不对称)
精度损失 数据不对称时较大 通常更小

4.3 量化参数的计算:scale 和 zero_point

刚才其实已经提到了计算方法。这里我再系统地总结一下,顺便加点实战经验。

4.3.1 确定数据范围

量化参数的计算,第一步是确定浮点数的范围 [x_min, x_max]。这个范围怎么来?

  • 权重:直接取模型权重的最小值和最大值。因为权重是固定的,训练完就不会变了。
  • 激活值:这个就有点讲究了。我建议用校准数据集(calibration dataset)跑一遍推理,收集每一层的激活值分布,然后取统计上的 min/max。有时候为了鲁棒性,也可以取百分位数(比如 99.9%),避免个别 outlier 把量化区间撑得太大。

小技巧:我在实际项目中,经常用滑动平均(moving average)来更新激活值的 min/max。这样即使输入数据有波动,量化参数也能自适应调整。

4.3.2 计算 scale 和 zero_point

有了范围,剩下的就是套公式了。我直接给个 Python 代码示例,方便你理解:

import numpy as np

def calc_quant_params(x_min, x_max, q_min=-128, q_max=127, symmetric=False):
    if symmetric:
        # 对称量化
        abs_max = max(abs(x_min), abs(x_max))
        scale = abs_max / ((q_max - q_min) / 2)
        zero_point = 0
    else:
        # 非对称量化
        scale = (x_max - x_min) / (q_max - q_min)
        zero_point = round(q_min - x_min / scale)
        # 确保 zero_point 在 [q_min, q_max] 范围内
        zero_point = max(q_min, min(q_max, zero_point))
    return scale, zero_point

# 示例
x_min, x_max = -5.6, 3.8
scale_sym, zp_sym = calc_quant_params(x_min, x_max, symmetric=True)
scale_asym, zp_asym = calc_quant_params(x_min, x_max, symmetric=False)

print(f"对称量化: scale={scale_sym:.4f}, zero_point={zp_sym}")
print(f"非对称量化: scale={scale_asym:.4f}, zero_point={zp_asym}")

输出结果:

对称量化: scale=0.0439, zero_point=0
非对称量化: scale=0.0369, zero_point=24

4.3.3 量化误差分析

量化一定会引入误差。误差主要来自两个方面:

  1. 截断误差:浮点数范围外的值被强行截断到边界。
  2. 舍入误差:浮点数映射到整数时的四舍五入。

我一般用「量化噪声」来衡量这个误差。说白了,就是量化前后的均方误差(MSE)。如果 MSE 太大,说明量化参数没选好,需要调整校准策略。

一句话总结:对称量化简单粗暴,适合权重;非对称量化灵活精准,适合激活值。选对量化方式,你的模型推理速度就能起飞,精度还能稳如老狗。

4.4 知识体系图

下面这张图,帮你把今天的内容串起来。从量化定义出发,到两种量化方式的对比,再到参数计算,一目了然。

模型量化原理(上)知识体系 模型量化 什么是量化? 对称量化 非对称量化 float32 → int8 映射 缩放 + 平移 zero_point = 0 适合权重(对称分布) zero_point 可调 适合激活值(不对称) 量化参数计算 scale(缩放因子) zero_point(零点)

好了,这一节的内容就到这儿。量化原理听起来抽象,但说白了就是「用整数近似浮点数」这么回事。下一节咱们会深入讲量化推理的具体实现,包括矩阵乘法怎么用 int8 算、怎么处理累加溢出等等。到时候你会发现,量化其实比想象中要简单得多。


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