1. 从一个反直觉的现象说起:为什么量化后的模型反而“更聪明”了
很多人第一次接触模型量化,脑子里冒出的第一个念头是:把高精度的浮点数砍成低比特整数,这不是在“降智”吗?精度掉了,模型还能用?我当初也是这么想的,直到有一次把一个视觉分类模型从FP32压到INT8,推理速度翻了将近三倍,准确率只掉了不到0.3个百分点,甚至在某个类别上还涨了一点。那一刻我才意识到,量化这件事远没有“砍精度换速度”这么简单,它背后有一套相当漂亮的数学逻辑在支撑。
这篇文章想做的事情,是把模型量化的数学原理从头到尾捋一遍。不是那种“调个库、传个参数就完事”的教程,而是真正搞清楚:为什么把连续的浮点数映射到离散的整数格点上,信息的损失可以被控制得这么小?量化误差到底从哪里来?对称量化和非对称量化的数学差异在哪?校准(Calibration)阶段到底在算什么?这些问题搞明白了,你在实际项目里做量化时就不会只是“试参数”,而是知道每一步在干什么、为什么这么干。
适合谁看?如果你已经用过一些量化工具,但对背后的数学一知半解,这篇就是写给你的。如果你还没接触过量化,但有一定的线性代数和概率基础,也能跟下来。我会尽量用生活化的类比把抽象概念讲清楚,同时保留必要的数学推导,让你不仅“会用”,还“懂为什么”。
2. 量化的本质:一个仿射映射的数学拆解
2.1 从浮点到整数:仿射变换的基本形式
量化的核心操作,用一句话概括就是:建立一个从浮点数到整数的仿射映射。所谓仿射映射,就是“乘以一个缩放因子,再加上一个偏移量”这种形式的变换。用数学语言写出来:
对于非对称量化:
q = round(x / s + z)其中 x 是原始浮点值,s 是缩放因子(scale),z 是零点(zero-point),q 是量化后的整数值。反量化的时候:
x_hat = s * (q - z)这里的 x_hat 是重建后的浮点值,它和原始值 x 之间的差就是量化误差。
对称量化是非对称量化的一个特例,它强制零点 z = 0,映射变成:
q = round(x / s) x_hat = s * q为什么要有对称和非对称两种?这取决于数据的分布。如果一个层的权重分布大致关于零对称(比如经过BatchNorm之后的权重),对称量化就够了,而且计算更简单。但如果数据分布明显偏向一侧(比如ReLU之后的激活值全是非负的),非对称量化能更充分地利用整数的表示范围。
2.2 缩放因子怎么算:从数据范围到整数范围
缩放因子 s 的计算,本质上是一个“范围映射”问题。假设浮点数据的动态范围是 [x_min, x_max],量化后的整数范围是 [q_min, q_max](比如INT8就是[-128, 127]),那么:
对于非对称量化:
s = (x_max - x_min) / (q_max - q_min) z = round(q_min - x_min / s)对于对称量化:
s = max(|x_min|, |x_max|) / q_max这里有一个很关键的细节:对称量化用 max(|x_min|, |x_max|) 而不是 (x_max - x_min)/2,是因为对称量化要求正负范围一致,所以取绝对值最大的那个作为边界。这会导致一个问题:如果数据分布严重不对称,对称量化会浪费一半的整数表示空间。举个例子,如果某层激活值范围是 [0, 6],对称量化会取 s = 6/127,那么所有负整数区间[-128, -1]全部浪费掉了,有效精度直接减半。
注意:选择对称还是非对称,不是拍脑袋决定的。权重量化通常用对称量化,因为权重分布一般关于零对称,而且对称量化可以让卷积运算中的零点乘法项消失,简化计算。激活量化通常用非对称量化,因为激活值经过ReLU之后是非负的,非对称能更充分利用位宽。
2.3 量化误差的数学表达:它到底损失了什么
量化误差 e = x - x_hat,在round操作的作用下,可以写成:
e = x - s * round(x / s + z) + s * z ...简化一下,对于对称量化,误差就是:
e = x - s * round(x / s)令 u = x / s,那么 e = s * (u - round(u))。由于 round(u) 和 u 之间的差最大不超过0.5,所以:
|e| <= s / 2这个结论非常重要:量化误差的绝对值上界就是缩放因子的一半。换句话说,缩放因子越小,量化误差越小。而缩放因子 s = range / (q_max - q_min),所以数据范围越小、整数位宽越大,量化误差就越小。
但这里有个陷阱:如果你为了减小 s 而强行缩小数据范围(比如做截断),那些被截断掉的值会产生更大的误差。这就是为什么量化里有一个专门的步骤叫“校准”,目的就是找到一个最优的截断范围,在“缩小s”和“减少截断损失”之间取得平衡。
3. 校准的数学:如何在截断与精度之间找平衡点
3.1 为什么不能直接用全局最大最小值
最朴素的量化方法是:统计整个张量的最大最小值,直接算 s 和 z。但这样做有个问题:深度网络中的激活值分布通常是“长尾”的,绝大多数值集中在一个较小的范围内,但偶尔会出现一些极大的离群值(outlier)。如果你用全局最大值来算 s,那么 s 会被这些离群值撑得很大,导致正常范围内的值量化误差剧增。
我做过一个实验:某个层的激活值99%都落在[-2, 2]之间,但最大值是15。如果用全局最大值,s = 15/127 ≈ 0.118,那么正常范围内的量化误差上界是0.059。但如果把范围截断到[-3, 3],s = 3/127 ≈ 0.0236,误差上界降到0.0118,缩小了5倍。代价是那1%超过3的值会被截断,产生较大的截断误差。但由于它们只占1%,总体误差反而更小。
3.2 KL散度校准:让量化后的分布尽量接近原始分布
既然截断有好处,那怎么找到最优的截断阈值?一个经典的方案是最小化量化前后分布的KL散度。具体做法是:
- 统计激活值的直方图,得到原始分布 P。
- 对于每一个候选截断阈值 t,把超过 t 的值截断,然后量化,再反量化,得到量化后的分布 Q_t。
- 计算 P 和 Q_t 的KL散度 D_KL(P || Q_t)。
- 选择使KL散度最小的那个 t。
KL散度的定义是:
D_KL(P || Q) = sum_i P(i) * log(P(i) / Q(i))它衡量的是:用分布 Q 来近似分布 P 时,信息损失有多大。KL散度越小,说明量化后的分布和原始分布越接近,量化带来的信息损失越小。
这个方法的直觉是:我们不关心单个值的误差,而是关心整体分布的保真度。即使某些离群值被截断了,只要整体分布形状没怎么变,量化后的模型表现就不会差太多。
3.3 百分位截断:一个更简单但有效的替代方案
KL散度校准虽然理论优雅,但计算量不小,而且对直方图的bin数量敏感。在实际项目中,我更多用的是百分位截断:直接取99.9%或99.99%分位数作为截断阈值。比如,把所有激活值排序,取第99.9%大的那个值作为 x_max。
这样做的好处是简单、快速、可解释。缺点是百分位这个超参数需要调,不同层、不同模型的最优百分位可能不一样。我的经验是:对于大多数视觉模型,99.9%是个不错的起点;对于NLP模型中的注意力层,可能需要更保守一些,用99.99%甚至更高,因为注意力分数的分布往往更分散。
实操心得:在校准阶段,不要只用一批数据。我通常会用100-200个batch的校准数据,统计每个层的激活值分布,然后取平均或中位数作为最终的截断阈值。用单batch校准容易过拟合到那批数据的分布,导致量化后在新数据上表现不稳定。
4. 量化粒度:从Per-Tensor到Per-Channel的数学差异
4.1 Per-Tensor量化:一个缩放因子管所有
Per-Tensor量化是最粗粒度的方案:整个张量共用同一个缩放因子 s 和零点 z。数学上最简单,计算也最快,因为反量化的时候只需要一次乘法。
但它的缺点也很明显:如果张量内不同通道的数据分布差异很大,Per-Tensor量化会顾此失彼。比如某个卷积层的权重,第一个通道的范围是[-0.1, 0.1],第二个通道的范围是[-2, 2],用同一个 s 的话,要么第一个通道的精度被浪费,要么第二个通道的精度不够。
4.2 Per-Channel量化:每个通道独立的缩放因子
Per-Channel量化给每个通道(通常是卷积核的输出通道)分配独立的 s 和 z。这样每个通道都能根据自己的数据范围来优化量化精度。
数学上,对于卷积权重 W,形状是 [C_out, C_in, K, K],Per-Channel量化会对每个 C_out 通道计算一组 (s_c, z_c):
s_c = max(|W[c, :, :, :]|) / q_max反量化的时候:
W_hat[c, :, :, :] = s_c * (Q[c, :, :, :] - z_c)Per-Channel量化的精度明显优于Per-Tensor,尤其是在权重分布通道间差异大的情况下。代价是存储量增加:每个通道要多存一个 s 和一个 z。对于INT8量化,权重本身占1字节,加上 s 和 z 的开销,总体存储增加大约 2/通道元素数,通常可以忽略不计。
4.3 Per-Group量化:在粒度和开销之间取折中
Per-Group量化是Per-Channel和Per-Tensor的折中:把通道分成若干组,每组共用一组 (s, z)。比如每128个通道一组。这样既比Per-Tensor精度高,又比Per-Channel开销小。
在大型语言模型的量化中,Per-Group量化非常常见。因为语言模型的权重矩阵往往很大,Per-Channel量化的存储开销会变得显著,而Per-Group能在精度和开销之间取得不错的平衡。
| 量化粒度 | 缩放因子数量 | 精度 | 存储开销 | 适用场景 |
|---|---|---|---|---|
| Per-Tensor | 1 | 低 | 最小 | 对精度要求不高的场景 |
| Per-Channel | C_out | 高 | 中等 | 卷积网络权重 |
| Per-Group | C_out / G | 中高 | 较小 | 大语言模型权重 |
注意:激活值量化通常只能用Per-Tensor,因为激活值是在推理时动态产生的,你没法提前知道每个通道的范围。有些方案会做动态量化,在推理时实时统计范围,但这会引入额外的计算开销。
5. 量化误差的传播:为什么深层网络对量化更敏感
5.1 误差累积的数学分析
量化误差不是孤立存在的,它会随着数据在网络中的传播而累积和放大。假设第 l 层的输入有一个误差 δ_l,经过该层的线性变换 W_l 后,输出误差变成:
δ_{l+1} = W_l * δ_l + ε_l其中 ε_l 是该层量化引入的新误差。展开这个递推关系:
δ_L = (prod_{i=l}^{L-1} W_i) * δ_l + sum_{j=l}^{L-1} (prod_{i=j+1}^{L-1} W_i) * ε_j第一项是初始误差经过所有层传播后的结果,第二项是各层量化误差的累积。如果权重矩阵的谱范数(最大奇异值)大于1,误差会被放大;如果小于1,误差会被衰减。
这就是为什么深层网络对量化更敏感:层数越多,误差累积的机会越多。而且如果某些层的权重谱范数较大,误差会被显著放大。
5.2 为什么BatchNorm对量化友好
BatchNorm层在训练时会归一化激活值,使得每层的输入分布保持稳定。从量化的角度看,BatchNorm有一个很重要的副作用:它把激活值的动态范围压缩到了一个相对固定的区间,通常是[-3, 3]左右(因为归一化后的值大致服从标准正态分布)。
这意味着量化BatchNorm之后的激活值,缩放因子 s 可以设得比较小,量化误差自然就小。而且由于分布稳定,校准阶段统计出来的截断阈值在推理时也能保持有效。
在实际量化中,通常会先把BatchNorm的参数融合到前面的卷积层里,这样推理时就少了一个算子,同时卷积权重的分布也会因为融合了BatchNorm的缩放而变得更适合量化。
5.3 残差连接:量化误差的“高速公路”
残差连接(Residual Connection)对量化误差的传播有特殊影响。残差连接的形式是:
y = F(x) + x如果 F(x) 的量化误差是 ε,那么输出误差就是 ε(因为 x 本身可能也有误差,但残差连接至少不会放大 F 的误差)。这看起来是好事,但实际上残差连接会让误差“跳过”一些层,直接传播到更深处。
更关键的是,残差连接要求 F(x) 和 x 的量化尺度要匹配。如果 F(x) 和 x 的分布差异很大,量化时可能需要不同的缩放因子,但相加操作又要求它们在同一尺度下进行。这就需要在量化方案设计时特别小心,通常会对残差连接的两个分支使用相同的量化参数。
6. 从数学到工程:量化实操中的几个关键决策
6.1 权重量化和激活量化为什么要分开处理
权重是静态的,在推理前就已知,所以可以用更精细的量化方案(比如Per-Channel),而且可以离线做复杂的校准和优化。激活值是动态的,每次推理都可能不同,所以只能用Per-Tensor,而且校准只能在有限的校准数据集上做。
这个差异导致了一个常见的工程实践:权重量化用INT8甚至INT4,激活量化用INT8,但两者的缩放因子和零点分开管理。在推理时,卷积运算变成:
y = s_w * s_a * conv(Q_w, Q_a) + ...其中 s_w 和 s_a 分别是权重和激活的缩放因子。这个乘法可以在后处理阶段合并,不会显著增加计算量。
6.2 量化感知训练:让模型“适应”量化误差
如果训练后量化(PTQ)的精度损失太大,可以考虑量化感知训练(QAT)。QAT的核心思想是在训练过程中模拟量化操作,让模型在训练时就“感受到”量化误差,从而调整权重来补偿。
数学上,QAT在前向传播时插入伪量化节点:
x_q = s * round(clamp(x / s, q_min, q_max))反向传播时,由于round函数的梯度几乎处处为零,需要用直通估计器(Straight-Through Estimator, STE):
dL/dx = dL/dx_q也就是说,梯度直接穿过round操作,不做任何修改。这看起来很不严谨,但实际效果出奇地好。直觉解释是:虽然round的梯度不对,但它至少给出了一个方向,让模型知道往哪个方向调整权重可以减少量化误差。
6.3 混合精度量化:不同层用不同位宽
不是所有层都同等重要。有些层对量化非常敏感(比如第一层和最后一层),有些层则很鲁棒(比如中间的卷积层)。混合精度量化就是给敏感层分配更高的位宽(比如INT16),给鲁棒层分配更低的位宽(比如INT4)。
决定哪些层用高位宽,通常有两种方法:一是基于敏感度分析,逐层量化并测量精度损失;二是基于权重分布,如果某层的权重分布很宽或者有很多离群值,就给它更高的位宽。
我在实际项目中的做法是:先用PTQ跑一遍,看每层的量化误差和最终精度损失,然后对误差最大的几层提升位宽,重新量化。通常只需要提升2-3层,就能把精度损失从1%降到0.1%以内。
实操心得:混合精度量化不要一开始就上,先用统一位宽跑通,找到瓶颈层之后再针对性调整。否则你会在参数组合的海洋里迷失方向。
7. 量化数学的边界:哪些情况下量化会失效
7.1 当数据分布极度不均匀时
如果某层的权重或激活值分布极度不均匀,比如99%的值集中在0附近,但1%的值跨度极大,那么无论怎么选截断阈值,都会面临两难:截断太多,那1%的信息丢失严重;截断太少,99%的值量化精度不够。
这种情况下,Per-Tensor量化基本无解,只能靠Per-Channel或Per-Group来缓解。如果连Per-Channel都不行,那可能需要考虑混合精度,给这层单独用更高的位宽。
7.2 当模型对数值精度极度敏感时
有些模型或任务对数值精度非常敏感,比如某些生成模型中的softmax层,或者某些强化学习中的值函数。这些地方的微小误差会被放大,导致输出完全不可用。
对于这种情况,我的建议是:不要强行量化这些层。保留它们为FP32或FP16,只量化那些鲁棒的层。混合精度量化框架通常都支持“排除某些层”的功能,善用它。
7.3 当校准数据分布与推理数据分布不一致时
校准阶段统计的激活值范围,是基于校准数据集的。如果推理时的数据分布和校准数据差异很大,那么校准阶段算出来的截断阈值可能完全不适用,导致量化误差剧增。
这个问题在领域偏移(Domain Shift)场景下特别常见。比如用自然图像校准的模型,部署到医学图像上,激活值分布可能完全不同。解决办法是:尽量用和目标域接近的数据做校准,或者使用动态量化,在推理时实时统计范围。
8. 我个人在量化实践中的几点体会
做了这么多量化项目,有几个体会是反复被验证的。
第一,量化不是“免费的午餐”,但它是一顿“性价比极高的午餐”。你几乎不可能做到量化后精度零损失,但你可以把损失控制在可接受的范围内,同时获得2-4倍的推理加速和4倍的模型压缩。这个 trade-off 在绝大多数部署场景下都是值得的。
第二,数学原理不是装饰品,它直接指导工程决策。理解了量化误差的上界是 s/2,你就知道为什么要做校准;理解了KL散度的含义,你就知道为什么截断比不截断好;理解了误差传播的递推公式,你就知道为什么深层网络要更小心。这些不是理论家的游戏,而是工程师的指南针。
第三,校准数据的质量和数量,往往比量化算法本身更重要。我见过太多人花大量时间调量化参数,却只用了几十张图片做校准。实际上,用200-500个batch的校准数据,比任何花哨的算法都管用。
第四,不要追求极致的低位宽。INT8已经能带来足够的收益,INT4虽然更激进,但精度损失和工程复杂度都会显著上升。除非你有极强的部署约束,否则INT8是大多数场景下的甜点。
最后分享一个小技巧:在做量化之前,先分析一下每层的权重和激活值分布。如果发现某些层的分布特别奇怪(比如双峰、长尾、极度偏斜),提前标记出来,这些层大概率是量化的难点。有针对性地处理这几层,比全局调参效率高得多。