3、模型存储机制:掌握模型参数在Flash中的存储方式,理解权重、激活值、缓冲区等不同数据的内存分配

TinyML 系统最根本的约束并非算力,而是内存与存储的物理极限。一颗典型的 Cortex-M4 微控制器可能只有 256KB Flash 和 64KB SRAM,而一个中等规模的 MobileNetV2 模型权重就超过 3MB。理解模型参数在 Flash 中的排布方式、激活值与中间缓冲区在 RAM 中的生命周期,是进行模型压缩、算子融合和内存规划的前提。本章将拆解 TinyML 模型在典型 MCU 上的存储拓扑,厘清权重、激活、缓冲区三类数据的本质差异,并给出可操作的内存估算与优化方法。

Flash 非易失存储(代码 + 权重) SRAM 易失存储(运行时数据) 模型结构描述 算子图 / 层配置 / 量化参数 通常 < 2KB 权重张量区 Conv/Dense 权重 + 偏置 占 Flash 总量 90% 以上 常量查找表(LUT) 激活函数表 / 反量化缩放因子 / 嵌入表 只读,可映射到 Flash 直接寻址 算子代码段 CMSIS-NN / 自研 kernel 的机器码 与权重分离存放,便于 OTA 差分更新 输入缓冲区 传感器原始数据 预处理后的张量 大小 = 输入维度 × 字节数 中间激活张量 每层输出特征图 内存峰值所在 需复用与规划 算子工作缓冲区(Scratch Buffer) im2col 重排 / 矩阵乘法累加器 / 量化偏移 由算子临时申请,生命周期最短 输出张量 + 后处理缓冲 分类结果 / 检测框 / 状态缓存 可覆盖输入缓冲区以节省空间 只读映射 查表 指令读取 Flash 访问特性 · 按页擦除(通常 4KB) · 随机读延迟 ~20ns,但写前需擦除 SRAM 访问特性 · 随机读写均 ~2-4ns · 容量小,但带宽远高于 Flash

上图展示了 TinyML 推理时 Flash 与 SRAM 的典型分工。Flash 侧存放模型结构描述、权重张量、常量查找表和算子代码,这些数据在推理过程中只读;SRAM 侧则承载输入缓冲区、中间激活张量、算子工作缓冲区和输出张量,这些数据在每次推理时被反复写入和覆盖。理解这张图的关键在于:Flash 决定模型能装多大,SRAM 决定模型能否跑起来。两者共同约束了 TinyML 模型的设计上限。

3.1 核心原理:三类数据的本质差异与生命周期

在 TinyML 系统中,数据按存储介质和生命周期可分为三个层次:持久只读数据(权重、偏置、查找表)、会话级数据(输入输出张量)、瞬时数据(算子中间结果)。这三类数据在 Flash 和 SRAM 中的分配策略截然不同。

权重参数是模型的核心,通常以量化后的 int8 或 uint8 格式存储。一个 3×3 卷积层,输入通道 32、输出通道 64,其权重数量为 3×3×32×64 = 18,432 个参数。若采用 int8 量化,则占用 18KB Flash;若采用 float32,则占用 72KB。权重在 Flash 中的排布必须遵循层序连续原则——即同一层的权重在物理地址上连续存放,以便 DMA 或 CPU 顺序读取,减少 Flash 随机访问带来的延迟惩罚。

激活值是推理过程中每层输出的特征图。以输入 96×96×3 的图像为例,经过第一层 16 通道卷积后,激活张量变为 96×96×16 = 147,456 个值,int8 格式下占用 144KB SRAM——这已经超过了许多 MCU 的 SRAM 总量。因此激活值必须采用内存复用策略:后一层的激活可以覆盖前一层不再需要的激活空间。TensorFlow Lite Micro 的 arena 机制正是基于这一原理,将整个推理过程所需的激活内存压缩到一个预分配的静态缓冲区中。

缓冲区则是最容易被忽视的部分。im2col 重排缓冲区、矩阵乘法的累加器(通常需要 int32 精度)、量化偏移表等,都属于算子工作缓冲区。这些缓冲区生命周期极短,仅在单个算子执行期间存在,但峰值可能相当可观。例如,一个 1×1 卷积若采用 im2col 优化,需要将输入特征图重排为 (输出像素数 × 输入通道数) 的矩阵,对于 32×32×16 的输入,im2col 缓冲区大小为 1024×16 = 16,384 个 int8 值,即 16KB——这比激活张量本身还大。

内存分配优先级原则:权重优先放入 Flash(只读,容量大),激活值在 SRAM 中复用(生命周期短,可覆盖),算子缓冲区按需申请(生命周期最短,可重叠)。任何试图将权重放入 SRAM 的做法都应视为设计缺陷,除非 Flash 容量确实不足且系统允许运行时解压。

3.2 判断方法:如何精确估算模型的内存占用

在开始移植模型之前,必须进行内存预算。以下是经过验证的四步估算法:

  1. 统计权重总量:遍历模型每一层,累加所有可训练参数(权重 + 偏置)的字节数。量化后 int8 每个参数占 1 字节,偏置通常为 int32 占 4 字节。例如 MobileNetV2 的 1.0 版本量化后权重约 3.4MB,偏置约 13KB。
  2. 计算激活峰值:逐层计算输出特征图大小,找出最大值。公式为:激活大小 = 输出高度 × 输出宽度 × 输出通道数 × 字节数。对于 96×96 输入、16 通道第一层,激活为 96×96×16×1 = 144KB。若采用深度可分离卷积,激活峰值通常出现在第一个全连接层之前。
  3. 估算算子工作区:检查每个算子是否需要额外缓冲区。卷积层若使用 im2col,需要 输入像素数 × 输入通道数 × 核大小² 字节;全连接层需要 批量大小 × 输入维度 字节。取所有算子中最大的工作区需求。
  4. 叠加系统开销:加上输入输出缓冲区(通常各占 1-4KB)、运行时栈(建议 2-4KB)、以及 RTOS 或调度器开销(若使用)。最终 SRAM 需求 = 激活峰值 + 最大算子工作区 + 系统开销。
数据类别存储介质生命周期典型大小优化策略
模型结构描述Flash持久0.5-2KB紧凑二进制格式,避免 JSON
权重张量Flash持久几十KB ~ 数MBint8 量化、剪枝、聚类
常量查找表Flash持久0.1-4KB映射到 Flash 直接寻址
输入缓冲区SRAM会话级1-100KB与输出缓冲区重叠
中间激活SRAM层间复用10-200KBarena 内存池复用
算子工作区SRAM瞬时1-50KB静态分配,避免堆碎片

实际工程中,建议使用脚本自动解析模型文件(如 TFLite 的 flatbuffer 格式),生成内存报告。TensorFlow Lite 的 interpreter.GetMemoryPlan() 接口可以直接输出各张量的内存分配情况,这是验证手工估算的最快途径。

3.3 案例拆解:关键词唤醒模型的存储优化实战

以一个典型的 2 秒关键词唤醒模型为例,输入为 40 维 MFCC 特征、时间步 49,模型结构为 2 层卷积 + 1 层 GRU + 全连接输出。初始模型使用 float32 存储,Flash 占用 1.2MB,SRAM 峰值 380KB,远超目标芯片(STM32L4 系列,512KB Flash / 128KB SRAM)的容量。

初始现象与约束:模型在 PC 上推理正常,但部署到 MCU 后编译失败,提示 Flash 溢出。进一步分析发现,GRU 层的权重占 480KB(float32),激活峰值出现在 GRU 层,为 49×128×4 字节 = 25KB(float32 下为 100KB)。

分析判断过程:首先将所有权重转为 int8 量化,Flash 占用降至 300KB,但 SRAM 峰值仍为 95KB(GRU 激活 + 工作区)。接着检查 GRU 的实现——标准 GRU 需要保存门控中间结果,若使用 TFLite 的 unidirectional_sequence_gru 算子,其工作区需要额外 49×128×3 个 int8 值(约 18KB)。此时 SRAM 总需求为 95 + 18 + 系统开销 10KB = 123KB,仍接近 128KB 上限。

实际操作步骤:

  1. 将 GRU 层替换为量化的 tf.lite.experimental.nn.GRU 版本,该版本将门控计算融合,工作区减少 40%。
  2. 将输入 MFCC 缓冲区从 40×49×4 字节(float32)改为 40×49×1 字节(int8),输入缓冲区从 7.8KB 降至 2KB。
  3. 将输出层与输入层共享同一块 SRAM 区域(推理结束后输入不再需要),节省 2KB。
  4. 将激活函数查找表(sigmoid/tanh)映射到 Flash 地址,通过 const 指针直接访问,不占用 SRAM。

结果对比:优化后 Flash 占用 312KB(含代码段),SRAM 峰值 86KB,编译通过且推理延迟从 210ms 降至 145ms(得益于 int8 的 SIMD 加速)。代价是精度从 94.2% 降至 93.1%,在可接受范围内。

复盘经验:内存优化不能只盯着权重,GRU/LSTM 类循环网络的工作区往往比激活更占内存。遇到 SRAM 瓶颈时,优先检查循环层的工作区实现,其次才是激活复用策略。

3.4 常见误区与总结复盘

误区一:认为 Flash 容量足够就万事大吉。Flash 只解决存储问题,不解决访问带宽问题。当权重超过 512KB 时,Flash 读取可能成为性能瓶颈,因为 Cortex-M 内核的 Flash 等待周期会随频率升高而增加。此时需要将热权重(如第一层卷积)缓存到 SRAM,或使用 Flash 加速器(如 ST 的 ART 加速器)。

误区二:动态内存分配(malloc)用于激活张量。TinyML 系统严禁在推理路径中使用堆分配,因为堆碎片会导致不可预测的失败。所有激活和缓冲区必须静态分配或使用内存池。TensorFlow Lite Micro 的 arena 机制就是完全静态的——在初始化时一次性分配最大需求,推理过程中零 malloc。

误区三:忽略 Flash 的擦写寿命。虽然推理时 Flash 只读,但若模型需要 OTA 更新,频繁擦写会损耗 Flash。设计时应将模型存储区与代码区分离,并使用差分更新策略,减少擦写次数。

误区四:所有数据都放在默认段。嵌入式链接脚本中,应显式将权重数组放入 .rodata 段,将激活缓冲区放入 .bss 段(零初始化),将大数组按对齐要求放置。使用 __attribute__((section(".rodata.model_weights"))) 可以精确控制存放位置,便于后续做 Flash 映射或外部 Flash 扩展。

风险提示:当 SRAM 需求超过物理容量时,不要试图通过减小栈空间来凑数。Cortex-M 内核的栈溢出会导致 HardFault,且难以排查。正确做法是重新设计模型结构(减少通道数、使用深度可分离卷积)或采用模型分区执行(将模型拆分为多个子图,分时加载到 SRAM)。