4、requantize概念:什么是requantize、为何需要、在推理中的位置、与量化参数的关系
在CMSIS-NN的推理加速体系中,requantize(再量化)是决定模型数值精度与推理效率的关键环节。它并非简单的“数据缩放”,而是发生在算子输出边界、连接不同量化精度域的核心数学操作。理解requantize,需要从量化神经网络的本质出发:当我们将浮点权重和激活映射到int8或int16定点数后,每一次卷积或全连接运算都会产生超出目标位宽精度的中间结果。此时,若直接截断或饱和处理,会导致严重的精度损失;若全部使用高精度累加,又会丧失CMSIS-NN在Cortex-M系列处理器上利用SIMD(单指令多数据)指令加速的优势。因此,requantize承担着“将高精度中间累加结果安全、高效地映射回低精度数值域”的桥梁作用。
上图展示了requantize在单层量化推理中的典型位置。从输入量化开始,数据经过算子计算产生int32精度的中间累加值,随后requantize将其转换为下一层所需的int8格式。这一过程并非可选项,而是量化推理的必经之路。下面我们从四个维度深入剖析requantize的本质。
4.1 核心原理:从数学本质到硬件约束
requantize的数学本质是仿射变换的逆过程。在量化推理中,每个张量都关联一组量化参数:scale(缩放因子)和zero_point(零点)。浮点值r与定点值q的关系为:r = scale * (q - zero_point)。当卷积层计算完成后,累加器中的int32值acc实际上是输入、权重和偏置的浮点运算结果在定点域的映射。为了将acc转换为下一层期望的int8格式q_out,需要执行以下数学变换:
q_out = clamp( round( acc * (scale_in * scale_w / scale_out) ) + zero_point_out, -128, 127 )
其中scale_in是输入激活的scale,scale_w是权重的scale,scale_out是输出激活的scale。由于scale_in * scale_w / scale_out在训练后是已知常量,CMSIS-NN会将其预计算为一个定点数multiplier和移位量shift,从而将浮点乘法转化为整数乘法和移位操作。这种设计完全服务于Cortex-M系列处理器:避免浮点运算,利用单周期乘法指令和桶形移位器,实现低延迟的数值转换。
4.2 为何需要requantize:精度与效率的平衡
在推理过程中,如果不进行requantize,有两种极端做法:一是保留int32累加结果直接传给下一层,这会导致数据位宽逐层膨胀,内存带宽和计算复杂度急剧上升,在资源受限的MCU上不可接受;二是直接截断int32的低16位,这虽然降低了数据量,但会引入巨大的量化噪声,导致模型精度崩溃。requantize提供了一条中间路径:通过合理的缩放和舍入,将信息损失控制在可接受范围内,同时保持数据位宽恒定。
从硬件角度看,CMSIS-NN的arm_convolve_s8等函数内部使用SMLAD或SMLALD指令进行int8乘累加,累加器为int32。如果每次卷积后都输出int32,那么下一层卷积的输入就需要重新量化,反而增加了额外开销。requantize在算子内部完成,使得层与层之间传递的始终是int8数据,这正好匹配了CMSIS-NN优化的数据通路设计。
| 对比维度 | 不进行requantize(保留int32) | 直接截断(无缩放) | 正确requantize |
|---|---|---|---|
| 数据位宽 | 逐层增长 | 保持int8 | 保持int8 |
| 精度损失 | 无额外损失 | 严重,噪声大 | 可控,接近最优 |
| 内存占用 | 指数级增长 | 低 | 低 |
| 计算效率 | 低,需额外处理 | 高但结果不可用 | 高且结果可靠 |
| 适用场景 | 理论分析 | 不推荐 | 生产环境 |
4.3 在推理中的位置:算子边界与数据流
requantize在推理图中的位置具有明确的规律性。它总是出现在以下两类边界:
- 算子输出边界:卷积、全连接、深度可分离卷积等算子的输出端。这些算子内部完成乘累加后,立即调用requantize函数(如
arm_requantize_s32)将int32结果转换为int8。 - 残差连接汇合点:当两个分支的数据相加时,如果两个分支的scale不同,需要先对其中一个分支进行requantize,使两者scale一致后再相加。这通常发生在ResNet等结构的add节点。
在CMSIS-NN的典型推理流程中,数据流为:int8输入 → 卷积(内部含requantize)→ int8激活 → 池化(无requantize)→ int8 → 下一个卷积。池化操作(如最大池化)不改变数值范围,因此不需要requantize;但平均池化涉及除法,可能需要额外的缩放处理。理解这一位置关系,有助于在部署模型时准确插入或调整requantize节点。
4.4 与量化参数的关系:scale、zero_point与multiplier
requantize的精度直接取决于量化参数的准确性。在训练后量化(PTQ)或量化感知训练(QAT)中,每个张量的scale和zero_point已确定。requantize的核心任务就是利用这些参数完成数值域映射。具体而言,CMSIS-NN使用以下方式优化计算:
// 预计算阶段(在模型初始化时完成)
float multiplier_f = (scale_in * scale_w) / scale_out;
int32_t shift = 0;
// 将multiplier_f规范化为[0.5, 1)范围的定点数
while (multiplier_f < 0.5f) {
multiplier_f *= 2;
shift--;
}
int32_t multiplier = (int32_t)round(multiplier_f * (1 << 31));
// 推理阶段(每个像素执行)
int32_t acc = ...; // 累加结果
int64_t temp = (int64_t)acc * multiplier;
temp >>= (31 - shift); // 算术右移
int32_t result = (int32_t)temp + zero_point_out;
result = MAX(MIN(result, 127), -128); // 饱和裁剪
这种实现方式将浮点乘法转化为一次32位乘法和一次移位,避免了除法运算。值得注意的是,shift可能为负数,此时需要左移操作,但CMSIS-NN通常要求shift在-31到+31之间,且使用64位中间变量防止溢出。此外,zero_point的引入是为了处理非对称量化,它使得浮点0可以精确映射到定点值,从而保证卷积中padding为0时计算结果正确。
4.5 案例拆解:一个典型卷积层的requantize调优
假设我们在一款Cortex-M7内核的MCU上部署一个二值化MobileNet模型。某层卷积的输入scale为0.0235,权重scale为0.0121,输出scale为0.0452,zero_point_out为-1。初始实现直接使用浮点计算requantize,发现单层推理耗时2.3ms,且精度比PC端低0.8%。通过分析,我们定位到问题:
- 现象:耗时过高,且输出激活值分布偏向负值,部分值被饱和裁剪到-128。
- 分析:浮点计算中,
multiplier_f = 0.0235 * 0.0121 / 0.0452 ≈ 0.00629,远小于0.5。预计算时,需要左移7次(乘以128)才能将multiplier_f提升到0.805,此时shift=-7。但浮点运算在MCU上开销大,且舍入误差累积。 - 优化:改用CMSIS-NN的定点实现,预计算multiplier=0.805*2^31≈1.73e9,shift=-7。推理时使用SMMUL指令完成32位乘法的最高32位结果,再右移7位。修改后耗时降至0.7ms。
- 精度问题:发现输出zero_point_out=-1导致所有负值被额外偏移,部分值超出[-128,127]范围。检查训练时的量化参数,发现该层激活值最小值为-0.5,最大值为6.2,但校准集未覆盖极端值。重新收集校准数据后,zero_point调整为0,精度损失降至0.2%。
这个案例说明,requantize的调优不仅是数学计算,还涉及量化参数的校准质量。实际部署时,应使用代表性数据集重新校准,并验证每个层的输出分布是否在int8范围内。
4.6 常见误区与总结复盘
在工程实践中,工程师常犯以下错误:
- 误区一:认为requantize只是简单的右移操作。实际上,必须考虑舍入误差和zero_point偏移,否则模型精度会下降。
- 误区二:忽略饱和裁剪。当累加值超出int8范围时,饱和裁剪是必要的,但若频繁触发饱和,说明scale设置不合理,应调整量化参数而非依赖裁剪。
- 误区三:在残差连接中直接相加两个不同scale的int8张量。这会导致数值错误,必须先统一scale。
- 误区四:使用浮点模拟requantize进行验证,但浮点舍入与定点舍入行为不同,可能导致部署后精度不一致。