嵌入式神经网络基础:基本算子与MCU计算特点
在深入CMSIS-NN的模型适配之前,必须先厘清神经网络在MCU上究竟是如何“运转”的。与云端GPU或边缘NPU不同,MCU没有大规模并行计算单元,也没有高带宽的DDR内存,其计算资源通常是一个主频在100MHz~400MHz的ARM Cortex-M内核,搭配几十到几百KB的SRAM和同等量级的Flash。因此,神经网络在MCU上的执行本质上是“用有限的算力去搬运和计算有限的数据”。本章将逐一回顾卷积、全连接、池化、激活函数这四个基本算子的数学定义与计算模式,并重点分析它们在MCU上的访存开销、计算密度和优化空间。理解这些底层特性,是判断一个网络结构是否适合在MCU上加速的前提。
上图展示了四个基本算子在MCU上的核心特征与约束条件。可以看到,卷积和全连接属于“计算密集+访存密集”型算子,而池化和激活则更偏向“访存密集”型。在后续的模型适配中,我们需要针对不同算子的特点采取不同的优化策略。下面逐一展开分析。
2.1 卷积算子:计算核心与访存瓶颈
核心原理
卷积是CNN中最主要的计算单元,其数学本质是输入特征图与卷积核之间的二维互相关运算。对于输入张量 \( X \in \mathbb{R}^{C_{in} \times H_{in} \times W_{in}} \),卷积核 \( W \in \mathbb{R}^{C_{out} \times C_{in} \times K_h \times K_w} \),输出特征图 \( Y \in \mathbb{R}^{C_{out} \times H_{out} \times W_{out}} \) 的每个元素计算如下:
Y[n][h][w] = bias[n] + Σ_{c=0}^{C_in-1} Σ_{i=0}^{K_h-1} Σ_{j=0}^{K_w-1}
X[c][h*stride + i - pad][w*stride + j - pad] * W[n][c][i][j]
在MCU上,这个公式意味着两层含义:计算量为 \( C_{out} \times H_{out} \times W_{out} \times C_{in} \times K_h \times K_w \) 次乘加(MACs);访存量则包括输入特征图的读取、权重矩阵的读取以及输出特征图的写回。以常见的3×3卷积、输入通道32、输出通道32、特征图尺寸32×32为例,单层计算量约为 32×32×32×32×9 ≈ 9.4M MACs,而输入数据量为 32×32×32 = 32KB,权重为 32×32×9 = 9KB。如果MCU主频为100MHz,每秒可执行约50M MACs(考虑双发射),则单层卷积需要约0.19秒——这还不包括数据搬运时间。
判断方法:计算密度与访存比
评估一个卷积层是否适合MCU加速,可以计算其算术强度(Arithmetic Intensity):
算术强度 = 总计算量(MACs) / 总访存量(Byte)
当算术强度大于MCU的“计算访存比”(即每秒可完成的MACs除以每秒可访问的字节数)时,该层为计算密集型,优化重点应放在提高MACs执行效率;反之则为访存密集型,优化重点应放在减少数据搬运。对于MCU,典型的计算访存比在 10~50 MACs/Byte 之间(取决于时钟频率和总线宽度)。
| 卷积类型 | 计算量(MACs) | 访存量(权重+输入) | 算术强度 | MCU适配性 |
|---|---|---|---|---|
| 1×1 Conv | 低 | 低(权重小) | 中等 | 适合,可做通道融合 |
| 3×3 Conv | 中等 | 中等 | 较高 | 最常用,CMSIS-NN深度优化 |
| 5×5 Conv | 高 | 高 | 中等 | 不推荐,可用两层3×3替代 |
| 深度可分离卷积 | 低 | 低 | 高 | 非常适合MCU |
案例拆解:3×3卷积在Cortex-M7上的优化
假设我们有一个输入为 32×32×16 的特征图,使用 16 个 3×3 卷积核,stride=1,padding=1,输出尺寸仍为 32×32×16。在未优化的C代码中,内层循环会逐元素计算,每次乘加都需要从内存加载输入和权重。CMSIS-NN的 arm_convolve_s8 函数则采用以下策略:
- 输入数据重排:将输入特征图按 2×2 或 4×4 的块进行重排,使得每次加载可以复用多个相邻像素。
- 权重重排:将权重矩阵按输出通道排列为 [C_out][K_h][K_w][C_in] 的格式,便于SIMD向量化加载。
- 寄存器分块:使用 Cortex-M7 的 SIMD 指令(如 SMLAD)一次执行 2 个 MACs,并通过循环展开减少分支开销。
实测结果表明,在 200MHz 的 STM32H743 上,该层从原始的 12ms 优化到 3.8ms,加速比约 3.2 倍。但需要注意的是,如果输入通道数小于 4,SIMD 的利用率会下降,此时优化效果不明显。
2.2 全连接层:权重主导的访存瓶颈
核心原理
全连接层本质上是矩阵向量乘:\( Y = W \cdot X + b \),其中 \( X \in \mathbb{R}^{N} \) 为输入向量,\( W \in \mathbb{R}^{M \times N} \) 为权重矩阵,输出 \( Y \in \mathbb{R}^{M} \)。在MCU上,全连接层通常出现在网络的最后几层,用于分类或回归。其计算量为 \( M \times N \) 次MACs,访存量主要为权重矩阵 \( M \times N \) 个参数。
以典型的 MNIST 分类网络为例,最后一层全连接可能为 512→10,权重仅 5120 个参数,约 5KB(int8)。但如果是一个 1024→1024 的全连接层,权重将达到 1M 参数,即 1MB 的 Flash 存储——这已经超出了许多MCU的Flash容量。因此,全连接层的瓶颈在于权重存储和读取,而非计算本身。
判断方法:权重驻留与Flash带宽
在MCU上执行全连接层时,权重通常存储在Flash中。每次计算一个输出神经元,需要读取 \( N \) 个权重值。如果Flash读取速度远低于CPU计算速度,则会出现“计算等待数据”的情况。解决思路有两种:
- 权重驻留SRAM:如果SRAM容量允许,将权重预加载到SRAM中,可显著提高读取速度。但SRAM容量有限,通常只能驻留部分层。
- 批量处理:将多个输入向量组成批量矩阵乘,提高权重复用率。但在MCU推理场景中,批量通常为1,因此权重复用率天然较低。
案例拆解:全连接层量化后的性能对比
假设一个 256→128 的全连接层,输入为 int8 量化数据。在 Cortex-M4 上,使用 CMSIS-NN 的 arm_fully_connected_s8 函数,权重以 int8 存储。实测计算时间约 0.15ms,而如果使用浮点权重(float32),时间将增加到 0.6ms,且Flash占用增加4倍。这说明量化对全连接层的加速效果非常显著,因为访存数据量减少了4倍。
2.3 池化层:轻计算重访存
核心原理
池化层(Pooling)用于下采样,常见的有最大池化(Max Pooling)和平均池化(Average Pooling)。其操作是在输入特征图的局部窗口内取最大值或平均值。池化层没有可学习的权重,计算量极小,但需要遍历整个输入特征图。
在MCU上,池化层的访存模式是顺序读取输入特征图,并写入输出特征图。对于 2×2 最大池化,stride=2,输出尺寸减半。其访存量约为输入特征图的大小,计算量仅为比较操作。因此,池化层是典型的访存密集型算子。
判断方法:池化层是否值得优化
由于池化层计算量小,通常不会成为性能瓶颈。但在某些极端情况下(如输入特征图很大、池化窗口很小),访存时间可能占据主导。此时可以考虑将池化操作与前一层的卷积或激活函数融合,减少一次完整的数据读写。
| 池化类型 | 计算量 | 访存量 | MCU优化建议 |
|---|---|---|---|
| 最大池化 2×2 | 3次比较/输出 | 输入特征图大小 | 可与ReLU融合 |
| 平均池化 2×2 | 3次加法+1次移位 | 输入特征图大小 | 注意整数溢出 |
| 全局平均池化 | H×W次加法 | 整个特征图 | 通常放在最后一层 |
案例拆解:池化与激活融合
在一个图像分类模型中,卷积层后紧跟ReLU激活和2×2最大池化。原始实现中,卷积输出写入SRAM,ReLU逐元素处理后再写回,池化再读取一次。三次访存共需读写 3×输出特征图大小。通过融合,可以在卷积计算完成后立即执行ReLU和池化,只写回池化后的结果,访存量减少约 2/3。在 64×64×32 的特征图上,这一优化节省了约 256KB 的读写流量。
2.4 激活函数:逐元素操作与查表法
核心原理
激活函数为网络引入非线性。常见的激活函数包括 ReLU(\( f(x)=\max(0,x) \))、ReLU6(\( f(x)=\min(\max(0,x),6) \))、Sigmoid(\( f(x)=1/(1+e^{-x}) \))和 Tanh。在MCU上,ReLU 系列只需比较和取最小值操作,计算开销极低;而 Sigmoid/Tanh 涉及浮点指数运算,在无 FPU 的MCU上非常耗时。
判断方法:激活函数的计算代价
对于 int8 量化模型,ReLU 可以直接在卷积输出后通过 max 指令完成,几乎零成本。而 Sigmoid 在 int8 域中通常使用查表法(Look-Up Table, LUT)实现:预先计算 256 个输入值对应的输出值,存为数组,运行时直接索引。查表法的精度取决于表的大小,通常 256 项即可满足分类任务需求。
// int8 Sigmoid 查表实现示例
static const int8_t sigmoid_lut[256] = { /* 预计算值 */ };
int8_t sigmoid_int8(int8_t x) {
return sigmoid_lut[(uint8_t)(x + 128)]; // 偏移到0~255
}
activation 参数指定。这样可以在卷积计算完成后立即对输出进行截断,无需额外的内存读写。对于Sigmoid等复杂激活,建议在网络结构设计时尽量避免,或使用ReLU替代。
常见误区与总结
- 误区一:认为所有算子都需要单独优化。实际上,算子融合(Conv+BN+ReLU、Conv+Pool)是MCU上最有效的优化手段。
- 误区二:忽视数据布局的影响。NHWC 与 NCHW 布局在MCU上的访存效率差异巨大,CMSIS-NN默认使用 NHWC,因为其通道连续性更好,便于SIMD处理。
- 误区三:只关注计算量而忽略Flash带宽。对于全连接层和1×1卷积,权重读取往往是真正的瓶颈。