矩阵乘法基础:数学定义、维度对齐、内存布局与ARM Cortex-M架构特点
矩阵乘法是CMSIS-NN中几乎所有深度学习算子的计算核心——全连接层、卷积层(通过im2col转化为矩阵乘法)、LSTM门控计算,最终都归结为对矩阵乘法的优化。在ARM Cortex-M系列处理器上编写高效的矩阵乘法kernel,首先需要透彻理解其数学本质、数据在内存中的排布方式,以及Cortex-M内核特有的硬件约束。本节将从这四个维度展开,为后续的kernel优化实操打下坚实的理论基础。
2.1 矩阵乘法的数学定义与计算本质
矩阵乘法定义本身并不复杂,但其计算模式决定了后续所有优化策略的方向。给定矩阵 A(维度 M×K)和矩阵 B(维度 K×N),乘积矩阵 C(维度 M×N)中的每个元素 C[i][j] 由 A 的第 i 行与 B 的第 j 列对应元素乘积之和构成:
C[i][j] = Σ_{k=0}^{K-1} A[i][k] · B[k][j]
从计算量角度看,完成一次 M×N×K 的矩阵乘法需要 M·N·K 次乘法和 M·N·(K-1) 次加法,即约 2·M·N·K 次浮点或定点运算。在CMSIS-NN的典型场景中,例如一个 8×8 的卷积核在 32×32 特征图上滑动,im2col后矩阵乘法维度可能达到 M=1024, N=64, K=64,单次推理就需要约 800 万次乘加运算。对于主频仅 100~200MHz 的Cortex-M4/M7处理器,这意味着必须充分利用每个时钟周期的计算能力。
2.2 维度对齐:从数学到内存的映射
维度对齐不仅是数学上的合法性检查,更直接影响kernel实现时的循环结构和数据复用策略。在CMSIS-NN中,矩阵乘法通常以 行主序(Row-major) 存储,即 A 的连续 K 个元素在内存中相邻,B 的连续 N 个元素在内存中相邻。这种布局下,计算 C[i][j] 时,A[i][k] 的访问是顺序的(k从0到K-1),而 B[k][j] 的访问则是跳跃的(步长为N)。
| 维度关系 | 数学要求 | 内存映射影响 | 优化含义 |
|---|---|---|---|
| A: M×K, B: K×N | 内维K必须相等 | A行连续,B列跳跃 | 需重排B或分块 |
| 输出C: M×N | 每个C[i][j]需K次乘加 | C行连续存储 | 累加器寄存器复用 |
| 批处理(Batch) | 多个独立矩阵乘 | 按batch维度连续 | 可并行/流水线处理 |
| 广播(Broadcast) | 维度为1时自动扩展 | 需显式复制或索引 | 避免实际复制,用偏移 |
在实际的CMSIS-NN代码中,arm_mat_mult_q15 函数要求输入矩阵按行主序排列,且输出矩阵的维度必须预先分配好。一个常见的错误是混淆了行主序与列主序的差异——如果数据来自外部传感器或某些Python框架(默认行主序),但kernel按列主序读取,结果将完全错误。维度对齐的另一个关键点是 对齐(Alignment):CMSIS-NN要求矩阵首地址至少4字节对齐(对于q15)或8字节对齐(对于q7),否则SIMD加载指令(如LDRD)会触发异常或性能下降。
2.3 内存布局:Row-major vs Column-major 的工程选择
内存布局决定了数据在物理内存中的排列顺序,直接影响cache命中率和SIMD向量化效率。ARM Cortex-M处理器通常没有复杂的分级cache(M0/M3无cache,M4/M7有零到两级cache),但数据总线宽度和突发传输特性使得连续内存访问远比随机访问高效。
- Row-major(行主序):A[i][k] 的连续元素在内存中相邻。访问A的一行时,可以顺序加载,非常适合SIMD。CMSIS-NN默认采用此布局,因为神经网络激活值通常按行存储(即特征图的通道维在内存中连续)。
- Column-major(列主序):B[k][j] 的连续元素在内存中相邻。如果直接按数学定义计算,访问B的一列时是连续的,但访问A的一行时是跳跃的。在CMSIS-NN中,通常需要对B进行转置(即预先存储B^T),使得B^T[k][j] 按行连续,从而在计算时两个操作数都能顺序访问。
以Cortex-M7为例,其总线支持 双发射(Dual-issue) 和 SIMD,但前提是指令操作数能快速从内存加载。如果矩阵B按列主序存储,每次计算C[i][j]时,B[k][j]的地址跳变N×sizeof(type)字节,导致每次加载都可能触发一次新的内存事务,严重降低有效带宽。因此,CMSIS-NN的矩阵乘法实现中,预先将B转置为行主序是标准做法,这虽然增加了一次数据拷贝的开销(O(K×N)),但换来了计算阶段的高效顺序访问。
// 示例:将B从列主序转为行主序(伪代码)
for (int j = 0; j < N; j++) {
for (int k = 0; k < K; k++) {
B_row_major[j * K + k] = B_col_major[k * N + j];
}
}
2.4 ARM Cortex-M架构特点对矩阵乘法的影响
Cortex-M系列处理器虽然共享精简指令集架构,但不同型号的计算能力差异显著。理解这些差异是选择优化策略的前提。
| 特性 | Cortex-M0/M0+ | Cortex-M3/M4 | Cortex-M7 | Cortex-M55/M85 |
|---|---|---|---|---|
| SIMD支持 | 无 | M4: 16×8/8×16 | 16×8/8×16 | MVE (128位) |
| MAC吞吐量 | 1 cycle/次 | 1 cycle/次 (SMLAD) | 2×MAC/cycle (双发射) | 4×MAC/cycle (MVE) |
| 数据总线宽度 | 32位 | 32位 | 64位 (AXI) | 64位 |
| Cache | 无 | 可选 | L1 I/D | L1 I/D |
| 乘加指令 | MULS+ADDS | SMLAD/SMLALD | SMLAD + 双发射 | VMLA (向量) |
对于Cortex-M4/M7,SMLAD(Signed Multiply Accumulate Dual)指令可以在单周期内完成两个16位乘法和两个32位累加。这意味着理论上每周期可完成2次MAC操作。然而,实际性能受限于:
- 操作数加载带宽:每周期需要从内存加载4字节(两个16位操作数),而Cortex-M4的数据总线为32位,刚好匹配。但若操作数未对齐或跨缓存行,则需额外周期。
- 累加器依赖:SMLAD的累加操作有1-2周期的流水线延迟,如果连续使用同一累加器,会产生停顿。优化策略是使用多个累加器交替计算不同输出元素。
- 循环开销:CMSIS-NN的kernel通常采用循环展开(unrolling)来减少循环控制指令(如CMP/BLE)的比例。例如,每次迭代计算4个输出元素,将循环次数减少4倍。
对于Cortex-M55/M85,ARMv8.1-M架构引入了 MVE(M-Profile Vector Extension),支持128位向量操作。一条 VMLA 指令可同时执行8个16位乘加,理论峰值达4 MACs/cycle(双发射时)。但MVE对数据对齐要求更高(需16字节对齐),且向量寄存器文件(8个128位寄存器)的管理需要更精细的调度。CMSIS-NN针对MVE提供了独立的优化路径,例如 arm_mat_mult_q15_mve 函数。