BatchNorm 变成乘加之后,真正的数值工作才开始

从固定统计量到通道乘加,再到乘数、移位和偏置。

Posted by Bruce Lee on 2026-02-02

系列目录 · 数值与量化 · Read in English

“BatchNorm 已经融合成 y=ax+b了,剩下不就一个乘法一个加法?”大家点头。几分钟后,问题变成:“a 用多少位?b 的 scale 是谁的?乘完先截断吗?a 接近 1 到底能不能省?”

这不是公式越来越复杂,而是公式终于遇到了有限位宽。

一、推理时的归一化可以折叠成仿射变换

固定推理统计量时,逐通道 BatchNorm 可写为:

1
2
3
4
y_c = gamma_c * (x_c - mean_c) / sqrt(var_c + epsilon) + beta_c
a_c = gamma_c / sqrt(var_c + epsilon)
b_c = beta_c - mean_c * a_c
y_c = a_c * x_c + b_c

这里统计量固定是前提。若处于训练阶段,需要从当前批次计算统计并更新状态,不能直接套用这一折叠。

历史中的相关实现承接的是这种仿射形式:读取乘法和偏置常量,生成整数常量及量化类型,再下降到目标计算。一个较早的方案使用专用仿射操作,后续转向通用乘加。两种方案都绕不开同一份数值契约。

二、参数自身也有量化域

设输入为 x=s_x(q_x-z_x),系数为 a=s_a(q_a-z_a)。即使 a是模型常量,它也不能简单截成整数后直接乘。

教学系数取 [-0.8,0.25,1.6]。若用对称有符号存储并选择某个正范围 Q,常见做法是:

1
2
s_a = max(abs(a)) / Q
q_a = clamp(round(a/s_a))

选择零点时,有符号对称域通常取零;无符号存储若要容纳正负系数,需要把零移到可用范围内部。历史常量量化逻辑根据存储符号与范围选择零点,以绝对值范围推导尺度,再舍入和夹取。

这种最大绝对值方案容易实现,但有一个取舍:一个特别大的通道会让所有小系数共用更粗的刻度。逐通道尺度可能改善误差,却需要后端支持更复杂的参数和广播。不能只把精度比较停留在“8 位还是 16 位”。

全零参数还需要专门定义 scale,避免除零。用正的兜底尺度表示全零是可行策略,但生成的整数值仍应等于表示实数零的零点,而不应把所有无符号整数都直接写成 0。

三、中间乘积落到哪里,决定偏置有没有机会补救

若把乘法结果立即量化到最终输出类型,再加偏置,可以写成:

1
2
q_mid = Q_out(a*x)
q_y = Q_out(D_out(q_mid) + b)

这里 Q 和 D 分别表示量化与反量化。若先在更宽域保留中间结果,只在最后量化:

1
q_y = Q_out(a*x + b)

两者一般不同。一个非常直观的教学例子:输出实数范围上界为 10,乘积为 12,偏置为 -4。分两步并在乘法后饱和,会先得到 10,再加 -4 得到 6;最后才饱和,则得到 8。后面的负偏置救不回已经被截掉的 2。

历史实现能看到乘法中间结果使用输出类型的路径。因此公开分析应提出中间范围与舍入点的验证问题,不能把代数相等当作逐位相等证明。是否可接受取决于目标量化规范和模型误差预算。

若使用更宽中间类型,会增加存储和带宽;若融合成一次仿射操作,可能减少一次量化,但需要执行单元或内核语义支持。选择哪条路线应同时列出精度、资源和维护成本。

四、“几乎等于一”并不总能安全省掉乘法

历史中曾用系数接近 1、偏置接近 0 的条件选择简化路径。数学上若 a=1,b=0,确实只剩输入到输出域的重量化;若 a=1,b≠0,可考虑只保留加法。

但“接近 1”是近似优化,它的误差与输入幅度有关:

1
省略乘法带来的实数误差 = (a-1)*x

如果 |a-1| 很小而 |x| 很大,误差仍可能跨过输出的一个或多个量化刻度。因此常数容差最好与数据范围或量化等价性联系起来,而不是独立拍一个数字。

更强的证明思路是:在所有允许输入整数上,原表达式和简化表达式产生相同输出整数。小位宽时可以穷举;更大空间则需要区间推理或有明确边界的误差分析。

后续历史版本调整了这些快捷分支,所以不能把某个中间版本的“近似恒等优化”描述成最终常驻能力。读提交链的意义之一,就是避免把曾经存在的路标当成今天的路。

五、将尺度吸附到二的幂:用精度换实现条件

另一处历史变化,在某个配置条件下把常量 scale吸附到二的幂。通用动机是让比例更适合移位或受限乘子路径,但具体收益需要看目标执行方式。

若原始尺度 s被提高到邻近的二次幂 s’,通常覆盖范围更宽,刻度也更粗。用最近舍入且未饱和时,单个常量量化误差的绝对值可粗略界为 s’/2;这只是常量本身误差,乘上激活后还会放大。

因此可以比较三种策略:自由尺度、二次幂尺度、逐通道尺度。比较内容不应只有最终平均误差,还应包含最大误差、极端通道、饱和比例和乘子能否被合法表示。某种模式“更容易生成”不是无代价优势。

六、一份数值回归矩阵

维度 待执行样例 要回答的问题
系数 负、零、很小、很大、接近一 符号与范围是否可靠
偏置 零、正负、与乘积相抵 中间饱和是否过早
参数形式 标量、逐通道、混合长度 广播是否符合语义
存储 不同位宽与符号 零点和夹取范围
尺度策略 自由尺度、二次幂尺度 误差与实现成本
边界 恰到饱和、舍入中点、实数零 舍入定义是否一致

参考实现应明确采用同样的推理统计、epsilon、舍入和饱和顺序;否则比较结果会把规范差异误判为内核错误。

性能方面,仿射运算通常需要逐元素读取和写入。若参数按通道复用,额外参数量随 C增长;若提前展开成完整特征图,额外量随 NCHW增长。下一篇将讨论为什么一段为了简单而写的广播代码,可能比乘加本身更值得优化。

乘加公式短,并不意味着工程只剩收尾。它恰好把所有数值责任集中到一个能写清、能验证的地方:常量怎么表示,中间在哪里舍入,最后在哪里饱和。

七、把参数误差沿公式传一遍

为了知道常量量化到底影响多少,可以把量化后的系数记为 a加上误差 da,偏置记为 b加上误差 db。忽略后续舍入和饱和时,输出误差就是 da乘输入,再加 db。因此小系数误差遇到大输入会放大,偏置误差则直接平移整条通道曲线。

这个关系给出一种很具体的测试设计:不要只随机抽取激活,而要分别在输入区间两端、接近零的位置取样。区间两端容易放大乘法系数误差,零附近容易看出偏置和零点问题。若每个通道的误差几乎是常量,先看偏置;若误差随输入线性增长,先看系数;若到某个幅度突然折断,先看饱和。它比只报告最大绝对误差更有诊断信息。

当采用统一参数尺度时,可以再按通道绘制系数量化前后的差异。大系数通道通常占据多数动态范围,小系数通道可能被量化到零。此时整体均方误差不一定很大,但某些通道的功能可能发生质变。把误差按通道分解,是检查逐通道仿射特别自然的一步,不能只依赖一份全张量平均值。

偏置的量化范围也不一定适合照搬激活的存储位宽。偏置可能用于抵消较大的均值项,实际数值比最终输出范围大。如果偏置常量自身在物化时先被夹取,后续加法即使完全正确,也只能处理已经损坏的参数。区分“常量存储时饱和”和“计算结果饱和”,有助于定位误差最早出现的阶段。

在设计评审中可以要求四个可见数:系数反量化误差、偏置反量化误差、乘法中间范围、最终输出饱和比例。前两项能通过常量生成过程检查,后两项需要结合输入范围或运行样本。只有一项通过,不能替代其他三项。

若要启用近似省略乘一的优化,也可以利用相同误差传递公式给出阈值:在已知输入绝对值上界时,系数偏差造成的最坏实数误差必须小于允许预算。若还要求整数逐位相同,就必须考虑舍入边界,单纯小于半个输出刻度仍不总足够,因为原结果可能恰好贴着边界。


返回系列目录 · 上一篇 · 下一篇


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !