4、CMSIS-NN库入门:CMSIS-NN架构、核心函数介绍、如何在项目中集成
说到在Cortex-M上跑神经网络,我第一个想到的就是CMSIS-NN。说实话,刚接触TinyML那会儿,我试过自己手写卷积、池化,结果性能惨不忍睹。后来才发现,ARM官方早就把这块优化好了——CMSIS-NN,就是专门给Cortex-M系列芯片准备的神经网络推理库。
你想想看,Cortex-M芯片就那么点Flash和RAM,还要跑神经网络,没有CMSIS-NN这种高度优化的库,基本是跑不动的。我个人的习惯是,只要项目里用到Cortex-M做推理,第一件事就是把CMSIS-NN集成进来。
CMSIS-NN架构概览
CMSIS-NN的架构其实不复杂。它主要分三层:
- 底层函数层:提供基础的数学运算,比如矩阵乘法、激活函数、池化操作。这些函数直接用汇编或者内联指令写的,充分利用了ARM的SIMD指令集。
- 中层函数层:封装了神经网络常用的层,比如卷积层、全连接层、池化层。你调用一个
arm_convolve_s8(),底层会自动调用多个优化过的子函数。 - 高层接口层:这部分其实不是CMSIS-NN的核心,但很多框架(比如TensorFlow Lite Micro)会基于它做封装。说白了,就是帮你把多个中层函数串起来,形成一个完整的推理流程。
我记得第一次看CMSIS-NN源码时,发现它大量使用了权重量化和对称量化技术。为什么?因为Cortex-M没有浮点运算单元(FPU)的型号太多了,用int8做运算比float快好几倍。嗯,这里要注意,CMSIS-NN默认就是int8量化模型,你如果拿float模型硬往上怼,性能会很难看。
核心函数介绍
我挑几个最常用的函数,给你讲讲它们到底是干什么的。
1. 卷积函数:arm_convolve_s8()
这是最核心的函数,没有之一。它实现了int8量化卷积。参数包括输入数据、权重、偏置、输出数据,还有卷积核大小、步长、填充方式等。
// 一个典型的调用示例
arm_cmsis_nn_status status;
arm_convolve_s8(&conv_params,
&quant_params,
input_data,
input_dims,
weights_data,
weights_dims,
bias_data,
bias_dims,
output_data,
output_dims);
我在项目中遇到过一个问题:输入数据的维度顺序搞反了。CMSIS-NN默认输入是CHW格式(通道、高度、宽度),而有些模型导出的是HWC格式。如果你不转换,推理结果就是错的。这个坑我踩过,后来写了个预处理函数专门做维度重排。
2. 全连接函数:arm_fully_connected_s8()
全连接层在分类网络的最后几层很常见。它的实现其实就是一个矩阵乘法加偏置。CMSIS-NN对这个函数做了极致优化,利用MVE指令(如果芯片支持的话)一次处理多个数据。
arm_fully_connected_s8(&fc_params,
&quant_params,
input_data,
input_dims,
weights_data,
weights_dims,
bias_data,
bias_dims,
output_data,
output_dims);
你可能会问:为什么全连接层也要单独优化?因为很多轻量级网络(比如MobileNet V1)最后几层就是全连接,如果这里慢了,整个推理时间就上去了。
3. 池化函数:arm_max_pool_s8() 和 arm_avg_pool_s8()
池化层用来降采样。CMSIS-NN提供了最大池化和平均池化两种。实现上,它用了滑动窗口的方式,但内部做了循环展开和指令优化。
arm_max_pool_s8(&pool_params,
input_data,
input_dims,
output_data,
output_dims);
我个人建议,能用最大池化就别用平均池化。为什么?因为最大池化只需要比较大小,而平均池化要做除法,虽然CMSIS-NN做了优化,但平均池化还是稍微慢一点。
4. 激活函数:arm_relu6_s8() 和 arm_tanh_s8()
激活函数在推理中也很关键。CMSIS-NN支持ReLU、ReLU6、Tanh、Sigmoid等。这些函数都是查表法实现的,速度很快。
如何在项目中集成CMSIS-NN
集成CMSIS-NN其实不复杂,但有几个关键步骤。我按顺序给你捋一遍。
第一步:获取CMSIS-NN源码
CMSIS-NN是ARM官方的开源库,你可以在GitHub上找到。我建议直接克隆整个CMSIS_5仓库,因为CMSIS-NN是CMSIS_5的一部分。
git clone https://github.com/ARM-software/CMSIS_5.git
你只需要把CMSIS/NN目录下的源文件复制到你的项目中。注意,还要包含CMSIS/Core和CMSIS/DSP的部分头文件,因为CMSIS-NN依赖它们。
第二步:配置编译器
CMSIS-NN对编译器有要求。我推荐用ARM Compiler 6或者GCC for ARM。如果你用IAR,也可以,但要注意优化等级要开到最高(比如-O3或-Ofast)。
另外,记得开启ARM的SIMD指令支持。对于Cortex-M4/M7,要定义__ARM_FEATURE_DSP;对于Cortex-M33/M55,要定义__ARM_FEATURE_MVE。
第三步:链接内存
CMSIS-NN的权重和偏置通常很大,我建议把它们放到Flash里,而不是RAM。因为Cortex-M的RAM通常只有几十KB到几百KB,放不下动辄几百KB的模型权重。
在链接脚本里,你可以这样定义:
/* 将权重数据放到Flash区域 */
.weights : {
*(.weights)
} > FLASH
/* 将输入输出缓冲区放到RAM区域 */
.buffer : {
*(.buffer)
} > RAM
然后在代码里用__attribute__((section(".weights")))来指定权重数组的存放位置。
第四步:编写推理代码
集成完成后,你就可以写推理代码了。我一般会封装一个model_inference()函数,里面依次调用卷积、池化、全连接等层。
void model_inference(int8_t *input, int8_t *output) {
// 第一层:卷积
arm_convolve_s8(...);
// 第二层:池化
arm_max_pool_s8(...);
// 第三层:全连接
arm_fully_connected_s8(...);
// 输出结果
memcpy(output, output_buffer, output_size);
}
嗯,这里要注意,每个层的输入输出缓冲区要提前分配好,而且大小要对齐。CMSIS-NN要求缓冲区地址是4字节对齐的,否则会触发硬件异常。
避坑指南
最后,我分享几个实战中遇到的坑,你遇到了可以少走弯路。
- 量化参数不匹配:CMSIS-NN的每个层都有自己的量化参数(scale和offset)。如果你从TensorFlow导出的模型量化参数和CMSIS-NN的不一致,推理结果会完全错误。我建议用
arm_nn_requantize()函数手动做一次重量化。 - 内存溢出:Cortex-M的RAM很宝贵。我曾经在一个项目中,因为临时缓冲区分配太大,导致堆栈溢出。后来我改用静态分配,并且仔细计算了每个层的最大内存需求。
- 性能瓶颈:如果你发现推理速度慢,可以用CMSIS-NN自带的性能测试函数
arm_nn_mat_mult_s8()来定位瓶颈。我遇到过卷积层太慢的情况,后来把卷积核从3x3改成1x1,速度提升了3倍。
好了,CMSIS-NN入门就讲到这里。下一章我会带你实际搭建一个完整的推理项目,从模型转换到板端运行,一步步走通。到时候你会觉得,原来在Cortex-M上跑神经网络也没那么难嘛。