3、CMSIS-NN矩阵乘法API概览:arm_mat_mult_q7/q15/q31函数签名、参数含义、支持的数据类型与精度

在嵌入式深度学习推理中,矩阵乘法是卷积、全连接层和注意力机制的核心计算原语。CMSIS-NN 针对 Arm Cortex-M 系列处理器提供了三套定点的矩阵乘法 API:arm_mat_mult_q7、arm_mat_mult_q15 与 arm_mat_mult_q31。这三套 API 共享相同的函数签名风格,但在数据宽度、累加器精度、饱和与移位策略上存在显著差异。理解这些差异,是进行 kernel 级优化的前提——选错 API 不仅会导致精度损失,还可能引入额外的周期开销。本章将逐一拆解函数签名、参数语义、数据类型适配规则,并通过一个实际的全连接层案例,演示如何根据输入分布和硬件特性做出正确选择。

CMSIS-NN 矩阵乘法 API 选型与数据流 输入矩阵 A M×K (q7/q15/q31) 输入矩阵 B K×N (q7/q15/q31) arm_mat_mult_q7/q15/q31 · 行主序存储 · 累加器宽度:q7→int32, q15→int64, q31→int64 · 输出移位与饱和(q7/q15) 输出矩阵 C M×N (q7/q15/q31) 选型判断路径 1. 输入范围是否在 [-128, 127] 且 K 较小? → 优先 q7(SIMD 吞吐翻倍) 2. 输入范围超过 q7 或需要更高中间精度? → q15(累加器 int64,防溢出) 3. 输入为高精度权重或激活(如 32 位特征)? → q31(最宽数据,但周期开销最大) 4. 所有 API 均要求行主序、对齐到 4 字节;q7 额外要求 K 为 4 的倍数。

上图展示了三个 API 的共性数据流:输入矩阵 A(M×K)与 B(K×N)经过内核计算得到输出 C(M×N)。所有 API 均采用行主序(row-major)存储,即同一行的元素在内存中连续排列。这一设计使得 CMSIS-NN 可以高效利用 Cortex-M 的 SIMD(单指令多数据)指令,例如 SMLAD(有符号乘加)和 SMLALD(双字乘加)。但三个 API 在累加器宽度、输出移位和饱和处理上存在本质区别,下面逐一深入。

3.1 函数签名与参数语义

三个 API 的函数签名结构高度一致,均遵循 CMSIS-DSP 的命名规范。以 arm_mat_mult_q7 为例,其完整声明如下:

arm_status arm_mat_mult_q7(
  const arm_matrix_instance_q7 *pSrcA,   // 指向矩阵 A 实例
  const arm_matrix_instance_q7 *pSrcB,   // 指向矩阵 B 实例
        arm_matrix_instance_q7 *pDstC,   // 指向输出矩阵 C 实例
  const q7_t *pState                      // 临时状态缓冲区(可为 NULL)
);

参数 pSrcA、pSrcB、pDstC 分别指向 arm_matrix_instance_q7 结构体。该结构体定义在 arm_math.h 中,包含三个字段:numRows(行数)、numCols(列数)和 pData(数据指针)。对于矩阵乘法,必须满足 A 的列数等于 B 的行数(即内维 K 相等),否则函数返回 ARM_MATH_SIZE_MISMATCH。第四个参数 pState 是临时缓冲区,在 q7 版本中用于存储中间累加结果,其大小至少为 2 * M * N 字节;若传入 NULL,函数内部会使用栈上分配,但可能增加栈压力。q15 和 q31 版本同样有 pState 参数,但 q31 版本中该参数被保留用于未来扩展,当前实现可传入 NULL。

API 名称输入类型输出类型累加器宽度输出移位饱和处理
arm_mat_mult_q7q7_t (int8)q7_t (int8)int32右移 7 位是(Q7 范围)
arm_mat_mult_q15q15_t (int16)q15_t (int16)int64右移 15 位是(Q15 范围)
arm_mat_mult_q31q31_t (int32)q31_t (int32)int64无(直接截断)否(需调用方处理)

从表中可以看出,q7 和 q15 版本在输出前会进行固定右移,这是为了将累加结果从高精度定点格式转换回目标位宽。例如,q7 版本中,两个 q7 数相乘得到 q14 格式(1 位符号 + 7 位小数 × 1 位符号 + 7 位小数 = 14 位小数),累加 K 次后,结果需要右移 7 位才能回到 Q7 格式。q15 版本同理,右移 15 位。而 q31 版本不做移位,因为两个 q31 数相乘得到 q62 格式,累加后直接截断为 q31 会丢失大量精度,因此该 API 更适合需要调用方自行处理缩放因子的场景。

3.2 数据类型与精度适配规则

选择哪个 API,本质上是在数据宽度、计算吞吐和精度之间做权衡。Cortex-M 内核(如 M4/M7/M33)的 SIMD 指令一次可处理 2 个 q15 或 4 个 q7 数据,因此 q7 版本的理论峰值吞吐是 q15 的 2 倍、q31 的 4 倍。但 q7 的动态范围只有 [-128, 127],如果输入数据的绝对值超过 127,必须预先做缩放(如除以 2 或 4),否则会引入严重的饱和误差。q15 的动态范围为 [-32768, 32767],适合大多数语音和传感器信号;q31 则覆盖 [-2^31, 2^31-1],适用于高动态范围的雷达或图像特征。

精度方面,q7 版本的累加器为 int32,当内维 K 较大时(例如 K=512),K 次乘加的最大累加值为 512 × 127 × 127 ≈ 8.25×10^6,远小于 int32 上限 2.1×10^9,因此不会溢出。但 q15 版本的累加器为 int64,这是必要的——因为两个 q15 相乘得到 q30 格式,K=512 时累加值可达 512 × 32767 × 32767 ≈ 5.5×10^11,超出 int32 范围。q31 版本同样使用 int64 累加器,但两个 q31 相乘得到 q62 格式,即使 K=2 也可能溢出 int64(最大 2^62 ≈ 4.6×10^18),因此 q31 版本要求调用方确保输入数据经过充分缩放,或者 K 值较小。

核心判断方法:在工程实践中,建议按以下步骤选择 API:
1. 测量输入数据的实际分布(最大值、最小值、标准差);
2. 若 max(|x|) ≤ 127 且 K ≤ 1024,优先选 q7;
3. 若 max(|x|) ≤ 32767 且 K ≤ 256,选 q15;
4. 若输入范围超过 q15 或需要保留更多小数位,选 q31,但必须手动管理缩放因子;
5. 始终检查输出是否饱和——CMSIS-NN 的 q7/q15 版本内置饱和指令(SSAT),但饱和会引入非线性失真,应通过合理的输入缩放避免。

3.3 案例拆解:全连接层中的 API 选型与调优

假设我们要在 Cortex-M7(主频 400MHz)上部署一个语音唤醒词检测模型,其中第一层全连接层的权重矩阵为 128×64(输入特征 64 维,输出 128 个神经元),激活函数为 ReLU。输入特征来自 MFCC(梅尔频率倒谱系数),经过量化后为 q7 格式,范围在 [-64, 63] 之间。权重经过训练后量化到 q7,范围在 [-32, 31]。内维 K=64。

初始现象:直接调用 arm_mat_mult_q7,发现推理时间约为 0.8ms,但输出结果与浮点参考相比,最大绝对误差达到 12(在 q7 尺度下),导致后续层误触发。

分析过程:检查输入分布,发现 MFCC 特征在静音帧时接近 0,但在语音帧时峰值可达 63,权重分布均匀。误差主要来自两个方面:一是 q7 乘法本身的量化误差(约 0.5 LSB),二是累加后右移 7 位时的截断误差。由于 K=64,累加值最大为 64×63×31 ≈ 125,000,右移 7 位后得到约 976,远超过 q7 的 127 上限,因此饱和频繁发生。

实际操作步骤:

  1. 将输入特征从 q7 缩放为 q15:将 MFCC 特征左移 8 位(乘以 256),范围变为 [-16384, 16128];
  2. 将权重从 q7 缩放为 q15:左移 8 位,范围变为 [-8192, 7936];
  3. 调用 arm_mat_mult_q15,此时累加器为 int64,最大累加值为 64×16128×7936 ≈ 8.2×10^9,远小于 int64 上限;
  4. 输出右移 15 位后得到 q15 结果,再右移 8 位(补偿输入缩放)并截断为 q7,最后应用 ReLU。

结果对比:优化后推理时间增加到 1.2ms(q15 的 SIMD 吞吐减半),但最大绝对误差降至 2,满足系统要求。若进一步将 K 从 64 扩展到 256(例如更大的全连接层),q15 版本依然安全,而 q7 版本则可能因 int32 累加器溢出(256×127×127 ≈ 4.1×10^6,仍安全,但饱和风险更高)。

风险与代价:q15 版本的内存占用翻倍(每个矩阵元素从 1 字节变为 2 字节),且计算周期增加约 50%。如果系统内存紧张,可考虑混合方案:输入保持 q7,权重提升为 q15,但 CMSIS-NN 不直接支持混合精度,需要手动拆分或使用 arm_mat_mult_q7 配合 arm_shift_q7 做后处理。

经验复盘:在嵌入式定点推理中,API 选型不是一次性的。当模型结构或输入分布变化时,应重新评估。建议在开发早期就编写一个自动化的精度评估脚本,对比定点输出与浮点参考的 SNR(信噪比),并设置阈值(如 SNR > 30dB)。此外,CMSIS-NN 的 q7 版本对内存对齐敏感——确保矩阵首地址 4 字节对齐,否则会触发慢速路径。

3.4 常见误区与总结复盘

误区一:认为 q31 版本精度一定最高。实际上,q31 版本不做输出移位,直接截断 int64 累加器到 int32,如果输入未经过适当缩放,截断误差可能远大于 q15 版本。q31 更适合输入本身就是 q31 格式(如某些雷达 FFT 输出)的场景。

误区二:忽略 pState 缓冲区的大小。q7 版本的 pState 需要 2*M*N 字节,如果传入过小的缓冲区,会导致内存越界。CMSIS-NN 文档中明确要求该缓冲区必须由调用方分配,且不能复用其他矩阵的数据区。

误区三:在 q7 版本中直接使用 K 不是 4 的倍数的矩阵。虽然 API 内部会处理边界,但性能会大幅下降。建议在模型转换阶段就将 K 填充到 4 的倍数(补零),例如将 K=66 填充到 68。

误区四:混淆定点格式与整数格式。q7 并不等于 int8,它表示 1 位符号 + 7 位小数。两个 q7 相乘得到 q14,累加后必须右移 7 位才能回到 q7。如果忘记移位,输出会放大 128 倍。