CMSIS-NN 核心功能:算子、量化参数与内存规划
核心功能:算子、量化参数与内存规划
CMSIS-NN 的公共接口集中在 arm_nnfunctions.h,参数结构体集中在 arm_nn_types.h 一类的类型头里。实际项目里最常打交道的算子如下:
| 算子函数 | 作用 | 参数要点 |
|---|---|---|
| arm_convolve_s8 | 标准二维卷积,int8 输入与权重 | 卷积参数、量化参数、输入、权重、偏置、输出的 dims,以及 im2col 用的 scratch buffer |
| arm_depthwise_conv_s8 | 深度可分离卷积的逐通道卷积,移动端网络的主力算子 | 输入通道数须与分组数一致;带 _opt 后缀的版本需要 scratch buffer |
| arm_fully_connected_s8 | 全连接层,本质是矩阵乘 | 输入、权重、偏置、输出四组 dims,权重按输出通道优先的布局排列 |
| arm_softmax_s8 | 分类头的 Softmax,int8 输入输出 | 需要整型乘子、移位,以及用于裁剪的差值阈值 |
| arm_max_pool_s8 / arm_avgpool_s8 | 最大池化与平均池化 | 窗口、步长、padding 与激活范围 |
| arm_add_s8 / arm_mul_s8 | 逐元素加法与乘法,用于残差连接、缩放 | 两个输入零点不同时要按格式做偏移补偿 |
量化怎么算。非对称线性量化的定义是:
real_value = scale * (q - zero_point)
q = round(real_value / scale) + zero_point
scale = (real_max - real_min) / (q_max - q_min)
zero_point = round(q_min - real_min / scale) // 再夹到 [q_min, q_max]
推理时不能用浮点除法去还原,所以算子内部把「输入 scale × 权重 scale ÷ 输出 scale」这个比例预先折算成一对整数:乘子 multiplier(定点小数形式)与右移位数 shift。运行时只做一次 32 位乘法加一次算术右移。CMSIS-NN 提供 requantize 类接口完成这一步(例如 arm_nn_requantize),但正常流程里这些参数由模型转换器导出,直接塞进 cmsis_nn_per_channel_quant_params 结构即可,不要手工推算——手算的乘子差一位,整层输出就会整体偏移。
整型范围与溢出。int8 卷积的中间累加结果是 32 位,偏置也是 32 位;深度可分离卷积因为逐通道累加,不同实现里累加器位宽可能不同。这意味着把输入零点设成接近 -128 这类偏大的值,会让累加更容易溢出。常见做法是给激活的量化范围留一点余量,而不是死磕满量程。
scratch buffer 与内存规划。这是 CMSIS-NN 与普通函数库最大的不同:算子需要调用方提供临时内存。接口上体现为一个 context 结构(形如 cmsis_nn_context,内含缓冲区指针与长度)。非优化版本可以传空,带 _opt 后缀的版本必须给。缓冲区大小由库提供的 buffer size 辅助函数计算,不要凭感觉写数字——不同版本、不同 padding 策略下的需求并不相同,以官方文档与所用版本的接口说明为准。
内存规划记三条:
- 权重常驻内存,放 flash 还是 RAM 要尽早定:放 flash 省 RAM,但访问延迟会影响吞吐,在带 cache 或预取的 M7 上差别明显。
- 激活张量按「同时存活的最大集合」复用同一块内存,不要给每层各分一块。这一般由上层框架(例如 TFLM 的 tensor arena)负责,手写推理时要自己算。
- scratch buffer 只要覆盖需求最大的那一层即可,各层顺序执行,可以共用同一块。
与 CMSIS-DSP 的关系。CMSIS-NN 的公共头文件依赖 CMSIS-DSP 的基础类型定义,较早的版本还需要链接一部分 CMSIS-DSP 实现。工程里若同时装了 CMSIS-DSP,注意两者版本能对上;只装 CMSIS-NN 时报类型未定义,先检查是不是缺了类型头文件。具体的依赖关系以官方文档为准。

