参数少的模型可能计算很多次,模型文件小的模型也可能产生很大的中间激活。把参数量、模型大小、MACC和激活内存混为一谈,会在部署阶段遇到Flash够用但RAM不足,或模型很小但推理超时的情况。
这四个指标需要分别计算,再与最终固件和目标MCU实测对应。静态分析帮助筛选候选模型,链接报告与板端测试负责最终验收。
| 参数量主要影响权重存储,MACC描述计算规模,激活内存决定运行峰值的一部分,模型文件还包含结构和元数据。 |
完成本篇后,你应该能够
参数是训练后保存的权重,激活是推理过程中产生的中间结果。前者多影响Flash,后者多影响运行时RAM。 |
图1:参数量、模型文件、MACC和激活内存对应不同资源问题
一、先把核心关系连起来
参数量先决定权重存储下限
全连接层参数量由输入维度和输出神经元相乘,再加偏置。卷积层参数量由卷积核尺寸、输入通道和输出通道决定,与输出时间长度无关。
权重采用FP32时每个参数通常占4字节,INT8权重通常占1字节。模型文件还包含图结构、张量描述和量化参数,因此不会严格等于参数量乘字节数。
| Dense参数量 = 输入维度 × 输出维度 + 输出偏置 Conv1D参数量 = 卷积核长度 × 输入通道 × 输出通道 + 输出偏置 |
MACC取决于算子被执行多少次
卷积核参数在每个输出位置重复使用,因此参数量不变时,输入长度越大,MACC越高。全局平均池化可以减少后续全连接参数,却仍需完成前面所有卷积计算。
MACC是比较计算规模的近似指标。内存访问、数据布局、量化重缩放和内核优化会让相同MACC产生不同执行时间。
表1:四个指标的含义与不能回答的问题
| 指标 | 主要回答 | 不能直接推出 |
|---|---|---|
| 参数量 | 权重数量 | 推理时间与Arena |
| 模型文件 | 序列化存储大小 | 最终固件与运行RAM |
| MACC | 理论计算规模 | 目标MCU上的真实耗时 |
| 激活内存 | 中间张量规模 | 完整系统RAM峰值 |
二、激活内存由张量形状和生命周期决定
每层输出张量的元素数乘数据类型字节数,给出该张量大小。运行时会复用生命周期不重叠的空间,因此峰值不是简单把所有层相加。
输入很长、早期通道数很宽的模型容易产生大激活。INT8能够减少激活字节数,但Scratch Buffer和对齐开销仍需由运行时实际规划。
Python:计算一层Conv1D的参数量与MACC
def conv1d_cost(input_length, in_channels, |
三、把静态指标映射到固件实测
模型报告应同时列出参数量、模型文件、MACC、最大单层激活和估算Arena。部署后补充最终固件Flash、Arena实际使用、推理时间和能量。
任何优化都要说明改善了哪一项。删除参数可能减少模型文件,却不一定降低推理时间;缩短输入可以同时降低MACC和激活,但可能损失事件信息。
四个指标来自网络的不同位置
Dense层输入I、输出O时,权重I×O、偏置O,主要MACC也是I×O。Conv1D还要乘输出时间位置:核宽K、输入通道Cin、输出通道Cout、输出长度Tout时,权重K×Cin×Cout,主要MACC为Tout×K×Cin×Cout。卷积参数共享使参数不随Tout增长,计算量却会随Tout增长。
模型文件大小受到数据类型和格式开销影响。10000个FP32权重的裸存储约40000字节,INT8约10000字节;偏置、量化参数、算子描述、张量元数据和对齐仍会占空间。最终Flash还包括运行时和已注册算子代码。
激活峰值来自执行时同时存活的张量与临时工作区。一个早期卷积层输出很长,即使权重很少,也可能成为Arena峰值。改变层顺序、步长或通道数会改变生命周期,需要运行时规划或目标板记录确认。
| Conv1D: Params = K×Cin×Cout + Cout MACC = Tout×K×Cin×Cout Activation bytes = Tout×Cout×bytes_per_element |
估算用于筛选,实测用于签字
训练前的公式可以快速淘汰明显超标结构,也能解释通道翻倍为何常使卷积计算接近四倍。它没有包含内核填充、内存复制、对齐和硬件指令效率,因此不能承诺延迟。
比较候选模型时固定输入、数据集、编译优化、主频、Cache、运行时版本和计时范围。记录模型文件、最终固件、Arena峰值、栈水位、预处理时间、Invoke时间和完整周期。
优化要针对瓶颈:Flash超限先看权重和算子代码;Arena超限先看激活形状;延迟超限看MACC、算子支持和内核效率;功耗超限还要看执行频率与待机。
指标、瓶颈和验证证据
| 指标 | 主要回答 | 最终证据 |
|---|---|---|
| 参数/模型文件 | 权重存储 | 文件与map报告 |
| MACC | 计算规模 | DWT或GPIO计时 |
| 激活大小 | Arena趋势 | 内存规划记录 |
| 栈/缓冲 | 系统并发峰值 | 栈水位与任务统计 |
动手:逐层生成模型资源账单
脚本实现Dense和Conv1D两种估算函数,输出参数、MACC和输出激活。随后比较两个候选模型,明确哪个受Flash约束、哪个受激活约束。
实验环境与输入
- Python 3标准库。
- 保存为 `model_cost.py` 并运行。
- 估算不包含运行时临时工作区,结果用于结构筛选。
按顺序完成实验
- 运行脚本,核对每层输入输出形状。
- 把第一层卷积通道从16改为32,观察参数、MACC和激活变化。
- 把stride从1改为2,观察输出长度与后续层成本下降。
- 为候选模型加入数据类型字节数,估算FP32与INT8激活差异。
可直接运行:Dense与Conv1D逐层资源估算
def dense(inputs, outputs, bytes_per=1): |
先读懂代码中的关键路径
- 每层成本函数显式返回输出shape,后续层必须读取前层输出而非手填长度。
- 参数与MACC分别累计,避免把一次权重存储误当成每个时间位置都存储。
- 最大列出激活只是下界,Arena还包括重叠、持久对象、对齐和scratch。
- stride改变输出长度时,要重新计算后续所有层,而非只修改当前层MACC。
你应该观察到什么
- 卷积参数量不乘输出长度,MACC会乘输出长度。
- 早期长序列卷积可能产生最大输出激活。
- stride减小后续时间长度,但也可能损失短暂事件。
成功标准
- 公式结果与模型摘要中的参数量一致。
- 估算报告明确区分可见张量与Arena实测峰值。
- 优化建议指向已识别瓶颈,不用模型文件大小替代全部资源。
失败时从哪里查起
资源估算偏差来源
| 现象 | 原因 | 检查 |
|---|---|---|
| 参数量差一个输出通道数 | 漏算偏置 | 确认层是否use_bias |
| MACC与工具不同 | padding或MAC定义不同 | 核对输出shape和计数口径 |
| Arena远大于最大激活 | 张量重叠与scratch | 查看运行时内存规划 |
把估算函数接入模型搜索后,可以在训练前过滤超预算结构,节省后续转换和上板时间。
把实验迁移到真实MCU项目
从Keras或TFLite摘要自动导出逐层shape后,再与公式脚本核对。工具口径不同的地方要记录,例如一次MAC是否算一还是两次操作。
板端报告用最终FlatBuffer和固件生成。训练模型参数量不包含转换融合、量化元数据和注册内核,不能直接代替Flash报告。
把结果再向前推进一步
- 手算一个3×6×16卷积层的参数与MACC。
- 找出你模型中最大的权重层和最大激活层。
- 为估算与实测分别定义报告字段。
收束:四个指标分别描述权重、文件、计算和中间张量。完整部署判断还需要最终固件、Arena、板端时间和功耗。
参考资料:
TensorFlow Lite Micro 官方代码仓库
Arm CMSIS-NN 官方文档