3. CMSIS-NN 核心数据结构:从类型到内存布局
做嵌入式AI部署,说白了就是跟数据结构和内存布局打交道。CMSIS-NN 这套库,我用了快五年了,每次移植新模型都得跟这些结构体碰面。今天咱们就把它的核心数据结构彻底捋清楚。
3.1 基础数据类型:q7_t、q15_t、q31_t
CMSIS-NN 没有用标准的 float 或 double,而是用了三种定长整型。为什么?因为 Cortex-M 内核没有 FPU 或者只有单精度 FPU,用整型做量化计算才能跑得快。
| 类型 | 位宽 | 取值范围 | 典型用途 |
|---|---|---|---|
q7_t |
8 位 | -128 ~ 127 | 激活值、权重(8位量化) |
q15_t |
16 位 | -32768 ~ 32767 | 中间累加结果、部分权重 |
q31_t |
32 位 | -2^31 ~ 2^31-1 | 偏置、累加器 |
我个人习惯把 q7_t 当作「主力选手」,因为 8 位量化在精度和速度之间平衡最好。q15_t 我一般用在需要更高精度的中间层,比如某些检测模型的头部。q31_t 嘛,几乎只用来存偏置——你想想看,偏置是累加后的结果,位宽不够会溢出。
__attribute__((aligned(8))) 声明。
3.2 卷积层结构体:cmsis_nn_conv_params
卷积是神经网络的主力,CMSIS-NN 用结构体把参数打包好。来看定义:
typedef struct {
int32_t input_offset; // 输入量化零点
int32_t output_offset; // 输出量化零点
int32_t multiplier; // 量化乘数(Q格式)
int32_t shift; // 量化移位量
int32_t activation_min; // 激活函数最小值(如ReLU的0)
int32_t activation_max; // 激活函数最大值
} cmsis_nn_conv_params;
这里有个关键点:multiplier 和 shift 是成对出现的。我曾经在项目里只改了 multiplier 忘了调 shift,结果输出全乱套。嗯,调试了两天才发现。
卷积核的权重内存布局是这样的:
// 权重排列:[输出通道][输入通道][核高][核宽]
// 实际存储时,CMSIS-NN 会按 HWC 顺序重排
q7_t weights[out_ch][in_ch][kernel_h][kernel_w];
为什么重排?因为 Cortex-M 的 SIMD 指令(如 SMLAD)喜欢连续的内存访问。把同一输出通道的权重连续放,能减少 cache miss。
3.3 池化层结构体:cmsis_nn_pool_params
池化层相对简单,但参数也不少:
typedef struct {
int32_t input_offset;
int32_t output_offset;
int32_t multiplier;
int32_t shift;
int32_t activation_min;
int32_t activation_max;
int32_t stride_width;
int32_t stride_height;
int32_t pool_width;
int32_t pool_height;
} cmsis_nn_pool_params;
注意看,池化层也有 multiplier 和 shift。为什么?因为平均池化做完除法后,结果还是量化值,需要重新缩放。最大池化则不需要——直接取最大值就行。
input_offset 设对。我曾经设成 0 而不是真正的零点,结果池化输出偏了半个刻度。
3.4 全连接层结构体:cmsis_nn_fc_params
全连接层其实就是矩阵乘法,但 CMSIS-NN 把它单独封装了:
typedef struct {
int32_t input_offset;
int32_t output_offset;
int32_t multiplier;
int32_t shift;
int32_t activation_min;
int32_t activation_max;
int32_t bias_offset; // 偏置的量化零点
} cmsis_nn_fc_params;
全连接层的权重布局和卷积不同:
// 权重:[输出神经元][输入神经元]
// 按行主序存储,方便矩阵乘法
q7_t weights[num_output][num_input];
我建议你把全连接层的权重单独拿出来做一次「转置检查」。因为有些训练框架(比如 TensorFlow)存的是列主序,而 CMSIS-NN 要行主序。不转置的话,推理结果就是错的。
3.5 权重与偏置的内存布局
这是最容易出问题的地方。CMSIS-NN 对内存布局有严格要求:
- 权重: 必须按
[输出通道][核高][核宽][输入通道]排列(即 NHWC 的变体)。 - 偏置: 每个输出通道一个 q31_t 值,连续存放。
- 对齐: 权重数组首地址必须 8 字节对齐,偏置数组 4 字节对齐即可。
来看一个实际的内存布局示例:
// 假设:输入通道=3,输出通道=2,核=3x3
// 权重内存(共 2*3*3*3 = 54 字节):
// [out0][h0][w0][in0] [out0][h0][w0][in1] [out0][h0][w0][in2]
// [out0][h0][w1][in0] ...
// ...
// [out1][h0][w0][in0] ...
// 偏置内存(共 2*4 = 8 字节):
// [bias0] [bias1]
3.6 整体数据结构关系图
下面这张图展示了 CMSIS-NN 各数据结构之间的关系:
3.7 实战中的内存布局检查
我在项目中总结了一套检查内存布局的方法:
- 打印首地址: 用
printf("%p", weights)确认对齐。 - 打印前几个值: 对比训练时的权重,看顺序对不对。
- 检查偏置符号: 偏置是 q31_t,如果符号位不对,输出会全偏。
好了,CMSIS-NN 的核心数据结构就这些。记住:类型选对、布局排对、对齐做对,你的模型部署就成功了一半。