4. 量化基础:定点数表示(Q7/Q15/Q31)、量化参数(scale/offset)、矩阵乘法中的溢出与饱和处理

在嵌入式平台(尤其是ARM Cortex-M系列)上部署神经网络时,浮点运算不仅速度慢,而且功耗高。CMSIS-NN库的核心设计思想之一,就是将神经网络中的权重、激活值和中间累加结果全部转换为定点数表示,利用硬件原生的整数乘加指令(如SMLAD、SMLALD)完成矩阵乘法。本章将深入剖析Q7/Q15/Q31定点格式的数学含义、量化参数(scale/offset)的映射关系,以及矩阵乘法中最棘手的溢出与饱和处理策略。

量化矩阵乘法数据通路与溢出风险 输入 A Q7 (int8) 权重 B Q7 (int8) 乘法器 Q7 × Q7 → Q15 中间值 累加器 Q31 (int32) ⚠ 溢出风险点 饱和处理 输出 C requantize → Q7/Q15 量化参数映射 scale = (max - min) / 255 offset = round(-min / scale) real_value = (q_value - offset) × scale 溢出防护 1. 累加前检查边界 2. 使用饱和指令 3. 合理选择Q格式

4.1 定点数表示:Q格式的数学本质

定点数是一种在固定位宽下,通过约定小数点位置来表示实数的方法。CMSIS-NN中主要使用三种格式:Q7(8位有符号整数,1位符号位+7位小数位)、Q15(16位有符号整数,1位符号位+15位小数位)、Q31(32位有符号整数,1位符号位+31位小数位)。这里的"Q"后面的数字表示小数部分的位数,而整数部分位数则由总位宽减去符号位和小数位得出。

以Q7为例,其数值范围是[-1, 1-2⁻⁷],精度为2⁻⁷≈0.0078125。这意味着一个浮点数0.5在Q7中表示为0b01000000(即64/128)。Q15的精度为2⁻¹⁵≈0.0000305,而Q31的精度高达2⁻³¹≈4.66×10⁻¹⁰。选择哪种格式取决于应用场景:权重和激活值通常用Q7以节省内存带宽,中间累加结果必须用Q31以防止溢出,部分特殊层(如LSTM)可能用Q15以平衡精度与范围。

格式 位宽 小数位 数值范围 精度 典型用途
Q7 8 7 [-1, 0.9921875] 2⁻⁷ 权重、激活输入
Q15 16 15 [-1, 0.999969] 2⁻¹⁵ 中间结果、部分递归层
Q31 32 31 [-1, 0.9999999995] 2⁻³¹ 累加器、高精度中间值

在CMSIS-NN的矩阵乘法实现中,输入数据通常以Q7格式存储,但乘法运算会自然产生Q15精度的中间结果(因为两个Q7数相乘,小数位相加:7+7=14,加上符号位后需要15位)。如果直接将Q15结果累加到Q31累加器中,需要将Q15左移16位对齐到Q31的小数点位置,或者采用更高效的SIMD指令同时处理多个Q7乘法。

核心原理:定点数乘法的精度损失是可控的。两个Q7数相乘得到Q14结果(实际存储为Q15),累加时使用Q31累加器可以容纳最多256次乘法累加而不溢出(因为Q7最大值为127,127×127×256≈4.1×10⁶,远小于2³¹≈2.1×10⁹)。这为深层网络的矩阵乘法提供了充足的安全余量。

4.2 量化参数:scale与offset的确定与使用

将浮点模型转换为定点模型时,需要为每个张量(权重、激活、输出)确定两个关键参数:scale(缩放因子)和offset(零点偏移)。它们建立了浮点实数与定点整数之间的仿射变换关系:

real_value = (q_value - offset) × scale

其中,scale是一个正浮点数,表示每个量化步长代表的实数范围;offset是一个整数,表示浮点值0对应的定点值。确定这两个参数的标准流程如下:

  1. 统计张量范围:收集训练后模型中该张量的最小值和最大值(min, max)。
  2. 计算scale:scale = (max - min) / (2^bits - 1),对于Q7格式,分母为255。
  3. 计算offset:offset = round(-min / scale),确保浮点0映射到整数offset。
  4. 量化与反量化:q = round(real / scale) + offset,反量化则使用上述逆变换。

在矩阵乘法中,量化参数的传递需要特别小心。假设输入A的量化参数为(scale_a, offset_a),权重B的为(scale_b, offset_b),输出C的为(scale_c, offset_c),则浮点运算:

C_real = Σ A_real × B_real

转换为定点运算后变为:

C_q = (scale_a × scale_b / scale_c) × Σ (A_q - offset_a) × (B_q - offset_b) + offset_c

其中scale_a × scale_b / scale_c是一个浮点乘数,CMSIS-NN将其转换为一个定点乘数(multiplier)和一个移位量(shift)的组合,以避免浮点运算。具体做法是将该浮点数表示为multiplier × 2^(-shift)的形式,其中multiplier用Q31格式存储,shift为整数。

实战经验:在CMSIS-NN的arm_convolve_s8函数中,量化参数通过arm_nn_requantize函数处理。该函数接受Q31累加值、multiplier和shift,输出Q7结果。关键优化点在于:当shift为正时,先做乘法再右移;当shift为负时,先左移再乘法,以最大化中间精度。

4.3 矩阵乘法中的溢出与饱和处理

矩阵乘法是神经网络中最核心的计算单元,也是溢出风险最高的地方。溢出发生在两个层面:乘法溢出和累加溢出。乘法溢出相对容易控制(Q7×Q7→Q15不会溢出),但累加溢出需要精心设计。

考虑一个典型的全连接层:输入向量长度为N,每个元素为Q7格式(最大127),权重也为Q7格式。单次乘法的最大值为127×127=16129,累加N次后最大值为16129×N。当N=256时,最大累加值为4,129,024,远小于Q31的最大值2,147,483,647,因此理论上不会溢出。但实际中,如果输入和权重都接近最大值(例如ReLU后的激活值),且N较大(如1024),则累加值可能达到16,516,096,仍然安全。然而,当使用Q15格式的权重时,情况就不同了:Q15×Q15→Q30,累加两次就可能溢出Q31。

风险提示:不要假设所有情况都不会溢出。在深度可分离卷积或分组卷积中,累加次数可能较少,但若使用Q15权重且输入未做归一化,溢出风险显著增加。CMSIS-NN提供了__SSAT指令(饱和指令)来应对溢出:当结果超出范围时,自动饱和到最大值或最小值,而不是回绕(wrap-around)。

饱和处理的实现方式有两种:硬件饱和指令和软件边界检查。ARM Cortex-M4/M7/M33等内核提供了__SSAT内建函数,它编译为单条SSAT指令,在结果超出指定位宽时自动饱和。例如:

int32_t acc = 0;
for (int i = 0; i < N; i++) {
    acc = __SMLAD(*(q15_t*)&A[i], *(q15_t*)&B[i], acc); // 乘加指令
}
// 饱和到Q7范围 [-128, 127]
int8_t result = (int8_t)__SSAT(acc, 8);

软件边界检查则适用于不支持SSAT指令的处理器,通过比较累加值与阈值来手动截断。虽然增加了分支指令,但在某些场景下更灵活(例如可以自定义饱和范围)。

4.4 案例拆解:8位卷积层的量化与溢出防护

下面通过一个具体的卷积层案例,完整展示量化参数计算、矩阵乘法实现和溢出防护的工程流程。

场景约束:目标平台为Cortex-M7(支持SIMD和饱和指令),输入特征图尺寸为32×32×16,卷积核3×3×16×32,步长1,无填充。浮点模型中,输入范围[-1.2, 2.8],权重范围[-0.5, 0.5],输出范围[-5.0, 5.0]。

量化参数计算:

  • 输入scale_a = (2.8 - (-1.2)) / 255 = 0.015686,offset_a = round(1.2 / 0.015686) = 76
  • 权重scale_b = (0.5 - (-0.5)) / 255 = 0.003922,offset_b = round(0.5 / 0.003922) = 127
  • 输出scale_c = (5.0 - (-5.0)) / 255 = 0.039216,offset_c = round(5.0 / 0.039216) = 127

定点乘数计算:浮点乘数 = scale_a × scale_b / scale_c = 0.015686 × 0.003922 / 0.039216 = 0.001568。将其表示为Q31格式:0.001568 × 2³¹ ≈ 3,367,000。为了保持精度,选择shift=20,则multiplier = round(0.001568 × 2²⁰) = 1,644。实际乘数 = 1,644 / 2²⁰ = 0.001568,误差小于0.01%。

初始现象:在未做饱和处理时,某些输出像素出现明显的白色噪点(值接近255),而周围像素正常。分析发现,这些位置的输入激活值恰好都处于高值区域(接近127),权重也多为正数,导致累加值达到约4,500,000,虽然未超过Q31上限,但在反量化时乘以scale_c后超出浮点范围。

分析判断:问题根源在于量化误差累积。虽然单次累加不溢出,但反量化后的浮点值可能超出训练时的动态范围。解决方案是在累加后、反量化前增加饱和步骤,将累加值限制在合理范围内。

实际操作步骤:

  1. 在卷积循环内,每次累加后使用__SSAT(acc, 16)将中间结果饱和到Q15范围(防止后续累加溢出)。
  2. 完成所有累加后,使用__SSAT(acc, 8)饱和到Q7范围。
  3. 应用requantize公式:result = (acc × multiplier) >> shift + offset_c,其中乘法使用Q31乘法,移位使用算术右移。

结果对比:加入饱和处理后,输出图像的噪点完全消失,PSNR从28.3dB提升到41.7dB,与浮点模型的输出差异小于0.5%。性能方面,由于SSAT指令是单周期的,整体推理时间仅增加2.3%。

风险与代价:饱和处理会截断极端值,可能导致梯度消失(在训练阶段),但在推理阶段影响极小。代价是需要额外2条SSAT指令,对于大模型会增加约1-2%的指令数。

复盘经验:量化模型的溢出防护不能只依赖理论计算,必须结合实际数据分布进行验证。建议在开发板上运行代表性输入,监控累加器的最大值和最小值,确保留有20%以上的安全余量。

4.5 常见误区与总结复盘

在CMSIS-NN量化开发中,工程师常犯以下错误:

  • 误区一:认为Q7×Q7→Q15永远不会溢出。实际上,当两个负数相乘时,结果为正,但绝对值可能超过Q15的最大值。例如(-128)×(-128)=16384,而Q15最大值为32767,虽然不溢出,但若累加两次就会超过。
  • 误区二:忽略offset的影响。在矩阵乘法中,如果直接使用量化后的整数进行乘加,而不先减去offset,会导致严重的精度损失。正确做法是先减去offset再进行乘加。
  • 误区三:使用相同的量化参数处理所有层。不同层的激活值分布差异很大,必须逐层统计并设置独立的scale和offset。
  • 误区四:混淆算术右移和逻辑右移。对于有符号负数,必须使用算术右移(>>),否则结果会出错。

最佳实践总结:(1)始终使用Q31累加器,并在每8-16次累加后做一次饱和检查;(2)量化参数计算时,使用校准数据集(而非训练集)统计范围,避免极端离群值影响;(3)在关键层(如第一层和最后一层)保留浮点计算或使用更高精度(Q15);(4)利用CMSIS-NN提供的arm_nn_requantize函数,它已经过充分优化,不要自行实现。