4、整型计算原理:量化矩阵乘法、卷积、加法的整型实现,如何避免浮点运算
TFLite Micro 在微控制器上部署模型时,最大的约束是缺乏 FPU(浮点运算单元)或浮点运算速度极慢。以 Cortex-M0+ 为例,一次浮点乘法需要调用软件库模拟,耗时可达数百个周期,而一次整型乘加(MLA)指令仅需 1 个周期。因此,量化推理的核心目标就是:将模型推理过程中的所有浮点运算,全部转换为整型(int8/uint8/int16)运算,同时保证数值精度损失在可接受范围内。本章将深入拆解量化矩阵乘法、卷积和加法的整型实现原理,揭示 Scale 与 Zero Point 如何在整型域中完成数学等价变换,并给出避免浮点运算的完整工程方法。
4.1 量化矩阵乘法的整型实现:从浮点公式到定点运算
矩阵乘法是神经网络中最基础的计算单元,全连接层、卷积层(经 im2col 变换后)以及注意力机制中的 QKV 变换,最终都归结为矩阵乘法。在浮点域中,矩阵乘法定义为 Y = W·X + b。当权重 W 和输入 X 分别以 Scale 和 Zero Point 量化后,我们需要推导出仅使用整型运算的等价形式。
设输入张量 X 的量化参数为 (S_x, Z_x),权重 W 的量化参数为 (S_w, Z_w),输出 Y 的量化参数为 (S_y, Z_y)。根据量化公式 r = S(q - Z),浮点矩阵乘法可展开为:
S_y(q_y - Z_y) = Σ S_w(q_w - Z_w) · S_x(q_x - Z_x) + b
将常数项提取出来,经过代数变换可得整型核心公式:
q_y = Z_y + M · Σ (q_w - Z_w)(q_x - Z_x) + b_int
其中 M = (S_w · S_x) / S_y,b_int = round(b / S_y)。这里的关键在于 M 是一个浮点数,但它在推理前是已知常量。TFLite Micro 采用 定点近似法:将 M 表示为 M ≈ m · 2^(-shift),其中 m 是一个 int32 整数(通常用 30 位精度表示),shift 是一个整数移位量。这样,浮点乘法 M · acc 就变成了整型乘法 (acc · m) >> shift,完全避免了浮点运算。
- 反量化输入:将 int8 输入减去零点
q_x - Z_x,得到有符号 int8 值(范围 -128~127)。 - 整型乘加:使用 int32 累加器执行
acc += (q_w - Z_w) * (q_x - Z_x),其中权重和输入均为 int8,乘积为 int16,累加为 int32,不会溢出。 - 应用乘数:计算
acc = (acc * m) >> shift,其中m为 int32 定点数,shift为右移位数。 - 加偏置与零点:
q_y = acc + b_int + Z_y,最后通过clamp将结果限制在 int8 范围内。
这种实现方式将每个输出元素的计算复杂度从浮点乘法(约 50~100 周期)降低到整型乘加(1~2 周期),在 Cortex-M4 上可提速 20~50 倍。需要注意的是,m 和 shift 的精度选择直接影响数值误差。TFLite Micro 默认使用 30 位精度表示 m,即 m = round(M · 2^30),然后根据 M 的大小调整 shift 使得 m 不超过 int32 范围。
4.2 量化卷积的整型实现:im2col 与直接卷积的取舍
卷积运算在量化后有两种主流实现路径:im2col + GEMM(矩阵乘法) 和 直接整型卷积。TFLite Micro 针对不同处理器架构和算子特点,会选择合适的路径。
im2col 方法将输入张量按卷积核大小展开为二维矩阵,每个卷积核窗口对应一行。例如输入尺寸 H×W×C,卷积核 K×K,输出通道 M,则 im2col 后的矩阵尺寸为 (H_out×W_out) × (K×K×C),权重矩阵为 M × (K×K×C)。这样卷积就转化为标准矩阵乘法,可以直接复用 4.1 节的整型 GEMM 实现。im2col 的代价是内存开销增大,但计算效率高,适合内存充足的场景。
直接整型卷积则不需要展开输入,而是通过循环遍历输出位置,直接计算每个输出像素的加权和。这种方法节省内存,但循环嵌套复杂,且难以利用处理器的 SIMD(单指令多数据)指令。在 TFLite Micro 中,对于 3×3 小卷积核且通道数较少的情况,直接卷积往往更高效;而对于大卷积核或深度卷积,im2col 更优。
| 对比维度 | im2col + GEMM | 直接整型卷积 |
|---|---|---|
| 内存占用 | 高(需存储展开矩阵) | 低(原位计算) |
| 计算效率 | 高(可优化为矩阵乘) | 中(循环开销大) |
| 适用场景 | 大卷积核、全连接层 | 小卷积核、内存受限 |
| 量化实现难度 | 低(复用 GEMM) | 高(需手动管理累加) |
无论采用哪种路径,卷积的量化核心公式与矩阵乘法一致,区别仅在于数据排列方式。在整型卷积中,累加器同样使用 int32,每个乘加操作均为 (q_w - Z_w) * (q_x - Z_x),最终统一应用 M 和 shift 进行缩放。
4.3 量化加法的整型实现:残差连接与特征融合
加法运算在 ResNet 的残差连接、FPN 的特征融合以及注意力机制的偏置添加中频繁出现。浮点加法 Y = X1 + X2 在量化后不能直接对 int8 值相加,因为两个输入的量化参数可能不同。设 X1 的量化参数为 (S1, Z1),X2 的为 (S2, Z2),输出 Y 的为 (S3, Z3),则浮点等式为:
S3(q3 - Z3) = S1(q1 - Z1) + S2(q2 - Z2)
整理后得到整型加法公式:
q3 = Z3 + M1·(q1 - Z1) + M2·(q2 - Z2)
其中 M1 = S1/S3,M2 = S2/S3。与矩阵乘法类似,M1 和 M2 在推理前预计算为定点数 (m1, shift1) 和 (m2, shift2)。实际计算时,先分别计算 t1 = (q1 - Z1) * m1 >> shift1 和 t2 = (q2 - Z2) * m2 >> shift2,然后 q3 = t1 + t2 + Z3,最后 clamp 到 int8 范围。
当两个输入的量化参数完全相同时(例如同一层的两个分支输出),M1 = M2 = 1,公式简化为 q3 = q1 + q2 - Z,此时可以直接使用 int16 加法避免溢出。TFLite Micro 会检测这种特殊情况并采用快速路径。
M1 和 M2 的 shift 不同,需要先对齐移位。例如 shift1=5,shift2=3,则先将 t2 左移 2 位再相加,或者统一使用较大的 shift 值。建议在模型转换时强制统一 shift,以减少运行时开销。
4.4 案例拆解:在 Cortex-M4 上部署 int8 卷积层
下面通过一个实际案例,完整展示整型卷积的部署过程。假设我们有一个 3×3 卷积层,输入通道 8,输出通道 16,输入特征图 32×32。模型使用 TensorFlow 训练后,经 TFLite Converter 量化,得到以下参数:
- 输入量化:
S_x=0.0078125(即 1/128),Z_x=-128 - 权重量化:
S_w=0.00390625(即 1/256),Z_w=0 - 输出量化:
S_y=0.0234375(即 3/128),Z_y=0 - 偏置:
b=0.5,量化后b_int = round(0.5/0.0234375) = 21
步骤1:预计算乘数 M
M = (S_w * S_x) / S_y = (0.00390625 * 0.0078125) / 0.0234375 = 0.0013020833
选择 shift=10,则 m = round(0.0013020833 * 2^10) = round(1.3333) = 1
实际 M_approx = 1 / 1024 = 0.0009765625,相对误差约 25%
这个误差偏大,说明 shift=10 精度不足。改用 shift=20:m = round(0.0013020833 * 2^20) = round(1365.33) = 1365,实际 M = 1365/1048576 = 0.0013017,误差小于 0.03%,满足要求。
步骤2:整型卷积计算
// 伪代码
int8_t input[32][32][8]; // 已减去 Z_x
int8_t weight[16][3][3][8]; // 已减去 Z_w
int32_t acc[16][30][30]; // 输出 30×30(无 padding)
for (int oc = 0; oc < 16; oc++) {
for (int i = 0; i < 30; i++) {
for (int j = 0; j < 30; j++) {
int32_t sum = 0;
for (int kh = 0; kh < 3; kh++) {
for (int kw = 0; kw < 3; kw++) {
for (int ic = 0; ic < 8; ic++) {
sum += (int32_t)weight[oc][kh][kw][ic] * input[i+kh][j+kw][ic];
}
}
}
// 应用乘数和偏置
int32_t scaled = (sum * 1365) >> 20;
int8_t out = (int8_t)(scaled + 21 + 0); // Z_y=0
output[oc][i][j] = out;
}
}
}
步骤3:性能对比与验证
在 100MHz 的 Cortex-M4 上,该层浮点实现耗时约 45ms,整型实现耗时约 2.1ms,加速比达 21 倍。数值精度方面,整型输出与浮点输出的最大误差为 0.8,平均误差 0.12,在 int8 量化可接受范围内(通常要求 <1.0)。
m 和 shift 的选择,确保误差在容忍范围内。另外,int32 累加器在极端情况下可能溢出(例如输入值全为 127,权重全为 127,3×3×8=72 次乘加,最大累加值 127*127*72 ≈ 1.16M,远小于 int32 上限 2.1G,安全)。
4.5 常见误区与总结复盘
在整型计算实现过程中,开发者常犯以下错误,需要特别警惕:
- 误区1:忽略 Zero Point 的减法。有些简化实现直接对 int8 值做乘加,忘记先减去零点。这会导致输出产生固定偏移,且该偏移随输入分布变化,无法通过偏置修正。
- 误区2:乘数 M 精度不足。使用过小的 shift 导致 m 的量化误差过大,尤其在多层网络中层间误差会累积。建议 shift 至少 15 位以上,并逐层验证。
- 误区3:累加器溢出。虽然 int32 通常足够,但在深度可分离卷积中,如果通道数很多(如 512),且输入值较大,累加值可能超过 int32 范围。此时需要分段累加或使用 int64。
- 误区4:加法中未对齐 shift。两个加数的 shift 不同时直接相加,导致结果错误。必须先对齐到同一 shift 再相加。
- 误区5:clamp 时机错误。应在所有运算完成后统一 clamp 到 int8 范围,而不是每步都 clamp,否则会引入额外截断误差。
总结复盘:整型计算的核心思想是将浮点运算中的 Scale 和 Zero Point 转化为预计算的定点乘数和移位操作。矩阵乘法、卷积和加法在数学本质上是一致的——都是先做整型乘加,再应用统一的定点缩放。TFLite Micro 通过这种设计,使得所有算子都能在无 FPU 的 MCU 上高效运行。工程实现时,应优先复用 TFLite Micro 提供的算子内核,避免自行实现;若需自定义算子,务必遵循上述定点计算流程,并通过单元测试验证数值一致性。
- 推理前预计算所有 M 和 shift,存入算子参数结构体。
- 输入和权重在加载时即完成零点减法,运行时不再重复。
- 使用 int32 累加器,必要时分段累加防溢出。
- 加法运算统一 shift 后再相加,优先使用相同量化参数的快速路径。
- 最终输出前统一 clamp,并配合 requantize 函数处理边界值。