把模型放上板:边缘 AI 的任务画像与可行性边界(MCU/NPU 实战视角)

内容分享2天前发布
0 0 0

把模型放上板:边缘 AI 的任务画像与可行性边界(MCU/NPU 实战视角)

关键词
TinyML、MCU、NPU、CMSIS-NN、TFLite Micro、算力预算、峰值激活内存、端到端延迟、量化、前处理开销、任务分级

摘要
边缘侧能不能跑 AI,不取决于“能不能训练”,而取决于任务画像资源预算是否被老老实实地做过:输入速率与分辨率、前处理算子链、网络 MACs/参数量、峰值激活内存、端到端时延、功耗与温升。这篇文章给出一套通用的方法:先用算力/内存/延迟三张预算表把任务刻画清楚,再把目标分成硬阈值/经典 DSP/轻量 DNN三个等级,逐步着陆到 MCU 或带 NPU 的 SoC。文中附带可直接复用的 Workload 基线与门槛线,避免“模型能跑起来但用不了”的情况。

目录

先画清任务:输入、速率、精度与失败代价
1.1 传感与数据通路画像(音频/IMU/摄像头)
1.2 指标口径:TPR/FPR、延迟预算、能耗预算
1.3 失败代价与降级路径(误报/漏报如何处理)

算力预算表:从 MACs 到周期
2.1 逐层 MACs 统计与等效吞吐(Hz→MAC/s)
2.2 MCU 与 NPU 的可用算力口径(主频×IPC×SIMD/加速核)
2.3 DSP/FFT/卷积的“热点”定位与替代算子(深度可分离、1×1、分组卷积)

内存预算表:参数、激活与中间缓冲
3.1 峰值激活内存的计算方法(逐层拓扑与复用)
3.2 im2col/FFT-conv 的额外缓冲与可关闭优化
3.3 SRAM/PSRAM/片外的读写代价与溢出保护

延迟预算表:端到端而非“单次推理”
4.1 前处理(重采样/MFCC/归一化)与 I/O DMA 的叠加
4.2 帧长/步长、批量与滑窗对实时性的影响
4.3 MCU 任务调度与中断抖动对闭环时延的占比

任务分级:先规则后模型,模型越小越好
5.1 硬阈值/统计量(能量、峰值、零交叉)
5.2 经典 DSP + 轻量分类器(SVM/朴素 Bayes)
5.3 轻量 DNN(DS-CNN、MobileNet 小型化、TCN)与量化优先级

平台画像:把模型拆到核上
6.1 Cortex-M(M4/M7)+ CMSIS-NN 的约束与玩法
6.2 带 NPU 的 SoC(卷积/激活/池化的 offload 边界)
6.3 存储布局与代码组织(常量/权重/激活分区)

基线与门槛:三类典型 Workload 的对齐方法
7.1 关键词唤醒、手势分类、低分辨率目标存在性
7.2 指标模板:p95 时延、峰值内存、每次推理能量
7.3 A/B:量化与剪枝对延迟/精度的实际收益

常见坑与止损清单
8.1 忽略前处理成本导致“推理快、端到端慢”
8.2 峰值激活估算错误、im2col 暴涨
8.3 片外内存抖动与 DMA/Cache 一致性问题
8.4 队列/缓冲策略错误引起的积压与功耗突增

1. 先把任务“画清楚”:输入、速率、精度与失败代价

1.1 任务画像清单(落表就能算)

把要做的边缘任务先量化到一张表,所有后续预算都从这张表推导。

字段 含义 示例(不要照抄,按你项目填)
输入模态 音频 / IMU / 图像 / 多模 单通道 16 kHz PCM / 6 轴 IMU 400 Hz / 160×160 灰度
前处理 重采样、窗函数、MFCC/FFT、归一化、降采样 25 ms 窗、10 ms 步,40 维 MFCC
推理周期 每秒触发次数(窗口+步长决定) 100 Hz(10 ms 步长)
目标指标 TPR/FPR、mAP、延迟门限 TPR≥0.95,FPR≤1%,端到端 ≤ 20 ms
决策代价 误报/漏报成本、降级策略 误报可容忍(报警闪烁),漏报不可接受(安全相关)
资源边界 峰值激活 SRAM、片外可用带宽、功耗 SRAM ≤ 256 KB;外设带宽 40 MB/s;能耗 ≤ 5 mJ/次
部署形态 MCU 裸核/RTOS / 带 NPU SoC M7 + CMSIS-NN / SoC NPU 卷积 offload

这张表的“推理周期”“延迟门限”“功耗”三项,是后续三张预算表(算力/内存/延迟)的约束来源。

1.2 数据通路与时间规范(端到端,而不是“单次推理”)


flowchart LR
  S[传感器 DMA] --> P[前处理
(重采样/MFCC/缩放)]
  P --> N[推理
(CMSIS-NN/TFLM/NPU)]
  N --> Q[后处理
(阈值/非极大抑制/投票)]
  Q --> D[决策/上报]

端到端延迟

L

L

L = 输入最后一个样本到达时刻 → 决策/上报时刻。推理周期

f

infer

f_{ ext{infer}}

finfer​:由帧长/步长决定;例如 25 ms/10 ms → 100 Hz。任何“能跑满帧”的算法,只要

L

L

L 超过表格里的门限(比如 20 ms),就是不可用。

1.3 把 KPI 变成可复现的数:统计规范

检测/分类:TPR/FPR(或 ROC/PR 曲线),阈值由验证集定。时延:记录 p50/p95/p99 的 端到端

L

L

L;推理时间只是

L

L

L 的一部分。内存:峰值激活(SRAM) + 权重量(Flash/PSRAM);记录最高水位能耗:单次推理能量

E

E

E(mJ/inf)=

I

(

t

)

V

d

t

int I(t),V,dt

∫I(t)Vdt;需要仪表采样或固件内采/算口径。稳定性:周期抖动

s

t

d

(

Δ

t

)

mathrm{std}(Delta t)

std(Δt)、异常触发次数(看门狗/重试/丢帧)。


2. 算力预算表:从 MACs 到周期与“屋脊线”(roofline)

2.1 逐层 MACs 公式(先算清“要干多少活”)

令输出特征图尺寸

H

o

×

W

o

H_o imes W_o

Ho​×Wo​,输入通道

C

i

C_i

Ci​,输出通道

C

o

C_o

Co​,卷积核

K

h

×

K

w

K_h imes K_w

Kh​×Kw​,分组

G

G

G:

标准卷积(Conv2D)

深度可分离(Depthwise)

Pointwise (1×1)

全连接(GEMM)

BN/激活/池化:近似为线性遍历(可按元素数 × 常数折算到“等效 MACs”或直接计字节搬运)。

先把模型逐层列出,累加得到一帧总 MACs:

M

A

C

s

total

mathrm{MACs}_{ ext{total}}

MACstotal​。

2.2 从 MACs → 周期:用“峰值 × 效率”而不是拍脑袋

核心关系

C

peak

C_{ ext{peak}}

Cpeak​:设备理论峰值(MAC/s),与主频、SIMD 宽度、DSP 指令管线相关。

η

compute

eta_{ ext{compute}}

ηcompute​:实测效率(0–1),和算子类型、数据布局、缓存命中强相关。

如何得到

η

eta

η(必须测,不要猜)
在目标板上做 3 个微基准,得到“每秒可完成的 MAC”:


conv3x3_dw
(深度卷积)
conv1x1_gemm
(pointwise → GEMM)
gemm_mnk
(确认小/中矩阵性能)
把测到的 MAC/s 除以理论

C

peak

C_{ ext{peak}}

Cpeak​,得到各算子族的

η

eta

η。预算时对每一层套它所属的

η

eta

η。

C++ 微基准骨架(CMSIS-NN/GEMM)


double bench_conv_dw3x3_q7(int H, int W, int C, int reps);
double bench_gemm_q7(int M, int N, int K, int reps);

struct KernelPerf { double conv3x3_dw_macs; double conv1x1_macs; double gemm_macs; double peak_macs; };
KernelPerf probe_device() {
  KernelPerf r{};
  r.conv3x3_dw_macs = bench_conv_dw3x3_q7(64,64,32,200); // 返回MAC/s
  r.conv1x1_macs    = bench_gemm_q7(64*64,32,16,400);
  r.gemm_macs       = bench_gemm_q7(128,128,64,200);
  r.peak_macs       = /* 主频×SIMD×(每拍MAC) 的理论值 */;
  return r;
}

把层映射到性能族并估时


struct Layer { enum Type{Conv,Depthwise,Pointwise,FC} type; int Ho,Wo,Ci,Co,Kh,Kw,group; };
struct Estimate { double macs, t_ms; };
Estimate estimate_layer(const Layer& L, const KernelPerf& perf){
  double macs=0, eta=0, peak=perf.peak_macs;
  switch(L.type){
    case Layer::Depthwise:
      macs = 1.0 * L.Ho*L.Wo*L.Ci*L.Kh*L.Kw;     eta = perf.conv3x3_dw_macs / peak; break;
    case Layer::Pointwise:
      macs = 1.0 * L.Ho*L.Wo*L.Ci*L.Co;          eta = perf.conv1x1_macs    / peak; break;
    case Layer::Conv:
      macs = 1.0 * L.Ho*L.Wo*L.Co*(L.Ci/L.group)*L.Kh*L.Kw; eta = perf.gemm_macs / peak; break;
    case Layer::FC:
      macs = 1.0 * L.Ci*L.Co;                    eta = perf.gemm_macs       / peak; break;
  }
  double t = macs / (peak * std::max(eta, 1e-3)); // 秒
  return {macs, t*1e3};
}

2.3 再加“屋脊线”:内存带宽可能才是瓶颈

很多小核 MCU 不是算力卡住,而是带宽/搬运卡住。用屋脊线模型估个下界:

算术强度

A

I

=

M

A

C

s

搬运字节数

AI = frac{mathrm{MACs}}{ ext{搬运字节数}}

AI=搬运字节数MACs​计算下界

T

comp

=

M

A

C

s

C

peak

T_{ ext{comp}} = frac{mathrm{MACs}}{C_{ ext{peak}}}

Tcomp​=Cpeak​MACs​带宽下界

T

mem

=

搬运字节数

B

peak

T_{ ext{mem}} = frac{ ext{搬运字节数}}{B_{ ext{peak}}}

Tmem​=Bpeak​搬运字节数​理论时间

T

max

(

T

comp

,

T

mem

)

T ge max(T_{ ext{comp}}, T_{ ext{mem}})

T≥max(Tcomp​,Tmem​)

实战里“搬运字节数”包括:输入/权重/输出 + im2col/FFT 临时缓冲。一旦

T

mem

T_{ ext{mem}}

Tmem​ 占主导,要优先做:

降分辨率/降通道(降低激活尺寸)优先使用 depthwise + pointwise(减少权重搬运)关闭/改写 im2col(换 direct/Winograd,视内核实现)

2.4 把预算合起来:一帧推理时间与算力缺口

总推理时间粗算:

端到端延迟预算:

L

>

L >

L> 门限:先改前处理/分辨率/步长(影响最大),再砍模型(深度可分离、1×1、剪枝/量化),最后才考虑换平台或 NPU offload。

2.5 直接可用的模型→预算脚手架(最小化版)

用一份 JSON 列表描述网络,导入→估算→出表。


struct BudgetRow { std::string name; double macs; double t_ms; size_t bytes; };
std::vector<BudgetRow> estimate_model(const std::vector<Layer>& net, const KernelPerf& perf, double B_peak){
  std::vector<BudgetRow> rows; rows.reserve(net.size());
  for (size_t i=0;i<net.size();++i){
    auto e = estimate_layer(net[i], perf);
    size_t bytes = /* 估算该层搬运(输入+权重+输出+临时) */;
    double t_mem_ms = 1000.0 * bytes / std::max(1.0, B_peak);
    rows.push_back({ "L"+std::to_string(i), e.macs, std::max(e.t_ms, t_mem_ms), bytes });
  }
  return rows;
}


rows
导成 CSV,附上 p95/p99 端到端的目标线,团队就知道“哪层在吃时间/吃带宽”,后续改动可量化对比。

2.6 三条“第一优先级”改造

前处理先降采:把 160×160 改 128×128(或音频 40 维 → 32 维),推理时间立刻成比例下降。把标准卷积替换成 depthwise+pointwise:MACs 大幅下降,激活/权重搬运也跟着降。量化优先:整条链(前处理/推理/后处理)统一到 int8,避免在板上来回量化/反量化。


3. 内存预算表:参数、激活与中间缓冲

3.1 三类内存分项与口径

权重/常量(Flash/ROM/PSRAM):模型参数、BN 常量、查表等。部署时对齐到 4/8 字节,利于 DMA 与总线访问。激活(SRAM,高优先):逐层中间特征图与其生命周期的峰值叠加,是实时性的硬约束。临时缓冲(SRAM):算子需要的工作区(如
im2col
、FFT、Winograd、cmsis-nn scratch),只在该算子生命周期内存在。

预算口径:峰值激活 + 峰值临时 ≤ 可用 SRAM(预留 ≥20% 余量给系统与栈)。

3.2 峰值激活的“生命周期”计算

将网络视作有向无环图(DAG)。每个张量

T

i

T_i

Ti​ 有生产时刻最后使用时刻,在这两个时刻之间占用内存。峰值激活为所有“同时存活”的张量之和的最大值。

ASCII 时序示意


Layer:   L1 ---- L2 ---- L3 ---- L4
Tensors:  T1====         
                   T2======      T4====
                         T3========/
峰值点:   ^ 在该时刻存活 {T1,T2,T3} 之和即为峰值之一

计算步骤

拓扑排序得到执行顺序;扫描图:遇到“生成”则加字节数,遇到“最后使用”则减字节数;过程中记录最大值即峰值激活。

C++ 片段(简化 DAG 生命周期统计)


struct Tensor { size_t bytes; int first, last; }; // first/last: 在拓扑序中的索引
size_t peak_activations(const std::vector<Tensor>& ts){
  struct E{ int idx, delta; size_t bytes; };
  std::vector<E> ev;
  ev.reserve(ts.size()*2);
  for (auto& t: ts){ ev.push_back({t.first, +1, t.bytes}); ev.push_back({t.last, -1, t.bytes}); }
  std::sort(ev.begin(), ev.end(), [](auto&a, auto&b){ return a.idx<b.idx || (a.idx==b.idx && a.delta>b.delta); });
  size_t cur=0, peak=0;
  for (auto& e: ev){ cur += (e.delta>0 ? e.bytes : (size_t)0); peak = std::max(peak, cur); if (e.delta<0) cur -= e.bytes; }
  return peak;
}

若框架支持激活复用/就地(in-place),在构图时将可复用的输入输出标记为同一缓冲,能显著压峰值。

3.3 典型算子的临时缓冲规模


im2col + GEMM
:临时缓冲约为

适用于标准卷积,在 SRAM 紧张场景建议禁用或切换 direct/Winograd 实现。

FFT/卷积:工作区大小与

N

log

N

Nlog N

NlogN 成比例;使用 CMSIS-DSP 时可预先查询
arm_cfft_sR_f32_lenXXX

bitrevTable
/
twiddle
需求并静态分配。

cmsis-nn scratch:大多 API 接收
cmsis_nn_context { void* buf; int32_t size; }
,在预初始化阶段先调用
*_get_buffer_size()
聚合出全网最大 scratch一次性静态分配

示例:cmsis-nn scratch 聚合


int32_t scratch_needed = 0;
scratch_needed = MAX(scratch_needed, arm_convolve_s8_get_buffer_size(&conv1_params));
scratch_needed = MAX(scratch_needed, arm_depthwise_conv_s8_get_buffer_size(&dw_params));
cmsis_nn_context ctx = { .buf = s_scratch, .size = scratch_needed }; // s_scratch 为全局静态缓冲

3.4 SRAM / PSRAM / 片外的取舍

SRAM(片上):低时延高带宽,用于激活与 scratchPSRAM(片外):大容量但带宽/时延劣化,适合权重;若必须放激活,需评估带宽受限 → 延迟上升Cache & DMA:有 D-Cache 的内核(如 M7)需给 DMA 缓冲做 cache 维护或放在非缓存区/DTCM,避免数据不一致。

3.5 预算表模板(填完就能拍板)

估算方法 数值
峰值激活(SRAM) 生命周期统计 … KB
峰值 scratch cmsis-nn / 算子查询最大值 … KB
权重(Flash/PSRAM) 模型大小 × 对齐 … KB
SRAM 目标 峰值激活 + scratch(+20% 余量) … KB
结论 是否满足 OK/FAIL

4. 延迟预算表:端到端,而非“单次推理”

4.1 组成项与算法性延迟

端到端延迟

L

L

L(最后一个样本到达决策输出)分解为:

L

frame

L_{ ext{frame}}

Lframe​:窗口/帧长带来的算法性等待(如音频 25 ms 窗,最后 10 ms 样本到达后仍需等窗满)。

L

pre

L_{ ext{pre}}

Lpre​:重采样、滤波、特征(MFCC/FFT/归一化)。

L

infer

L_{ ext{infer}}

Linfer​:推理时间(受算力/带宽/量化影响)。

L

post

L_{ ext{post}}

Lpost​:后处理(阈值、NMS、投票/去抖)。

L

sched

L_{ ext{sched}}

Lsched​:任务切换、DMA 同步、互斥/队列等待等。

窗口与步长对实时性的影响

帧长

W

W

W,步长

H

H

H(滑窗):吞吐 = 采样率/H算法性延迟

W

approx W

≈W重叠越小(H 大),吞吐越低,但鲁棒性可能下降;平衡点需要用验证集确定。

4.2 串行 vs 流水:时间线

串行(简单但慢)


|---- 收满帧 W ----|--Pre--|--Infer--|--Post--|   L 串行 = W + Pre + Infer + Post + Sched

流水(推荐)


t0:    [收集 W] 
t0+H:           [收集下一帧] [Pre] [Infer] [Post]
t0+2H:                                [Pre] [Infer] [Post]
稳态 L 流水 ≈ max(W, Pre+Infer+Post) + Sched

稳态下,若
Pre+Infer+Post < W
,端到端主要由帧长决定;否则由计算链决定。

Gantt(Mermaid)

4.3 MCU 端测量:硬件周期计数器

Cortex-M DWT CYCCNT(常用)


static inline void dwt_init(void){
  CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
static inline uint32_t dwt_cycles(void){ return DWT->CYCCNT; } // 主频换算时间

测量前后读取
CYCCNT
,换算为 µs;在 FreeRTOS 中把前处理/推理/后处理各自埋点,形成端到端

L

L

L 的构成。

FreeRTOS 定时口径


uint32_t t0 = dwt_cycles();
// ... 前处理/推理/后处理 ...
uint32_t t1 = dwt_cycles();
uint32_t us = (t1 - t0) / (SystemCoreClock / 1000000U);

4.4 主机/SoC 侧测量:稳态统计

使用
steady_clock
统计 p50/p95/p99;窗口 ≥ 1–3 分钟(前 10–30 s 预热丢弃)。记录每帧
frame_id, t_last_sample, t_pre_end, t_infer_end, t_post_end, t_decision
,导出 CSV。

C++ 订阅端(示例)


struct Row{ double t_s, L_ms, pre_ms, infer_ms, post_ms; };
std::vector<Row> rows;
auto tick = [&](double pre, double infer, double post, rclcpp::Time t_last){
  auto now = node->now(); double L = (now - t_last).seconds()*1e3;
  rows.push_back({now.seconds(), L, pre, infer, post});
};

4.5 预算表模板(端到端)

数值 备注
帧长

W

W

W

… ms 音频/图像窗口
Pre … ms 目标板实测 p95
Infer … ms 目标板实测 p95
Post … ms 目标板实测 p95
Sched … ms 抢占/互斥平均开销(p95)

L

steady

L_{ ext{steady}}

Lsteady​

max(W, Pre+Infer+Post)+Sched 判定是否达标

4.6 优化顺序(影响从大到小,通常)

缩窗/增步(W↓/H↑):优先受控;代价是鲁棒性,需以指标验证。降分辨/降通道:激活与 MACs 成比例下降。量化 int8 并确保前/后处理也在 int8 上,避免频繁量/反量化。算子替换:标准卷积 → depthwise + pointwise;Winograd/直接卷积替换
im2col
流水化:并行 I/O 与计算(DMA + 中断/任务分工)。约束
L_sched
:提升相关任务优先级,避免低优先级占用临界区。


5. 任务分级:先规则后模型,模型越小越好

5.1 判别“能不用模型就不用”的决策树

目的:把 60–90% 的“无事样本”拦在 DNN 之前,节省 MACs 与 SRAM 峰值。

轻判常用量

音频:帧能量

E

=

x

2

E=sum x^2

E=∑x2、过零率 ZCR、谱质心 SC。IMU:模长

a

|a|

∣a∣、角速度阈值、峰-峰值、步频。视觉:像素方差、帧差计数、亮度直方图熵。

阈值制定:用验证集离线扫描阈值曲线(TPR/FPR),目标是FPR 极低(宁可放过,交给 DNN)。


5.2 规则/统计量实现(int16 整型路径)

音频帧能量与过零率(Q15)


// x: Q15, len=N
typedef struct { uint32_t energy; uint16_t zcr; } vad_feat_t;
vad_feat_t vad_feat_q15(const int16_t* x, int N){
  uint64_t e = 0; uint16_t z=0;
  int16_t prev = x[0];
  for (int i=0;i<N;i++){
    e += (int32_t)x[i]*(int32_t)x[i]; // 32x32->64
    if ((x[i]^prev) < 0) z++; // 符号变化
    prev = x[i];
  }
  // 规约到口径:能量/样本,过零率/帧
  vad_feat_t r = { .energy = (uint32_t)(e / (uint32_t)N), .zcr = z };
  return r;
}

IMU 模长与峰-峰值(float 或 Q15 均可)


typedef struct { float a_rms, gyr_pp; } imu_feat_t;
imu_feat_t imu_feat(const float ax[], const float ay[], const float az[],
                    const float gx[], const float gy[], const float gz[], int N){
  double s=0; float gmin=1e9f,gmax=-1e9f;
  for (int i=0;i<N;i++){
    float a = ax[i]*ax[i] + ay[i]*ay[i] + az[i]*az[i];
    s += a;
    float gmag = fabsf(gx[i]) + fabsf(gy[i]) + fabsf(gz[i]);
    gmin = fminf(gmin, gmag); gmax = fmaxf(gmax, gmag);
  }
  imu_feat_t r = { .a_rms = sqrtf((float)(s/N)), .gyr_pp = (gmax-gmin) };
  return r;
}

判别门控(极轻量)


bool pass_gate_audio(vad_feat_t f){ return (f.energy > E_MIN) || (f.zcr > Z_MIN); }
bool pass_gate_imu(imu_feat_t f){ return (f.a_rms > A_MIN) || (f.gyr_pp > GPP_MIN); }

只在“疑似有事件”的帧上跑 DNN,其余直接输出“无事件”。


5.3 经典 DSP + 轻量分类器(不用卷积也能凑够效果)

特征:音频(能量、ZCR、MFCC 8–20 维)、IMU(均值、方差、谱能量带、步频)。分类器:线性 SVM/逻辑回归/朴素 Bayes/小型决策树(存储几十到上百字节)。存算口径:特征维度

d

d

d,分类器系数

O

(

d

)

O(d)

O(d);SRAM 峰值几乎不变,推理仅

O

(

d

)

O(d)

O(d) 乘加。

逻辑回归(浮点/定点皆可)


float lr_infer(const float* x, const float* w, float b, int d){
  float z=b; for(int i=0;i<d;i++) z += w[i]*x[i];
  // 近似 sigmoid,3段分段线性避免 exp
  if (z < -4.f) return 0.f;
  if (z >  4.f) return 1.f;
  return 0.5f + z*(1.f/8.f); // 粗略近似即可做门控
}

何时选这条路:当正/负例可分性强、输入噪声可控、误报代价低时,常常“DSP+线性”比小型 DNN 更省电且足够稳。


5.4 轻量 DNN:只拿能吃下的算子

结构首选:DS-CNN / MobileNet(α<0.5,输入 96×96/128×128)/ TCN(1D Dilated conv)。量化全整型 int8(权重 per-channel,激活 per-tensor)→ CMSIS-NN/TFLM 直跑;标定集必须覆盖真实分布。算子白名单(嵌入式实现最稳):
conv2d
(standard/depthwise)、
add

mul

relu/relu6

avg/maxpool

fully_connected
。避免
reshape
引入大拷贝、避免
softmax
放到板上(阈值化即可)。

CMSIS-NN int8 卷积调用轮廓


// 关键是一次性准备好 context.scratch,权重/偏置对齐到 4/8 字节
arm_status s = arm_convolve_s8(&ctx, &conv_params, &quant_params,
                               &input, &filter, &bias, &output);

剪枝/瘦身的三把刀

宽度系数 α(0.25–0.5);输入分辨率(128→96 或 64);用 DW+PW 替换标准卷积(MACs 降数倍)。

做任何一个压缩动作前,先看端到端延迟表里占比谁最高:若前处理占大头,先缩窗/降步;若推理占大头,再砍模型。


5.5 分级落库模板(一体化开关)


typedef enum { PIPE_RULE_ONLY, PIPE_RULE_PLUS_LIN, PIPE_LIGHT_DNN } pipeline_t;
typedef struct { pipeline_t mode; uint16_t E_MIN, Z_MIN; float prob_on; } cfg_t;

bool pipeline_run(const int16_t* audio_q15, int N, const cfg_t* cfg){
  vad_feat_t f = vad_feat_q15(audio_q15, N);
  if (!pass_gate_audio(f)) return false;       // 规则挡住大部分
  if (cfg->mode == PIPE_RULE_PLUS_LIN){
    float feat[12]; extract_feats(audio_q15, N, feat, 12);
    float p = lr_infer(feat, W, b, 12);
    return p > cfg->prob_on;
  }else if (cfg->mode == PIPE_LIGHT_DNN){
    int8_t in[96*96]; preprocess(audio_q15, N, in);
    int8_t out[2]; run_dscnn_int8(in, out);
    return out[1] > THRESH;
  }
  return true;
}

同一套接口,随配置切换路线,便于 A/B 对比与现场降级。


6. 平台画像:把模型拆到核上

6.1 Cortex-M(M4/M7)路线图

数据类型:优先 int8;必要时中间累加用 int32,避免溢出。

库选择:CMSIS-NN(卷积/FC/激活/池化)、CMSIS-DSP(FFT/MFCC/滤波)。

存储布局

激活 + scratchSRAM/DTCM权重尽量放 Flash(支持 XIP)并按层搬运到 SRAM(分批)以减片外抖动。对齐到 4/8 字节,便于 DMA/总线突发。

Cache/DMA(M7 常见)

DMA 读写缓冲放非缓存区或做
Clean/Invalidate
;避免 cacheline 伪共享;批量计算阶段关闭中断或缩短临界区。

任务调度

FreeRTOS:
preproc_task > infer_task > log_task
;推理段尽量避免阻塞调用;使用双缓冲(Ping-Pong)在采集/前处理与推理之间解耦。

链接脚本/段属性(示例)


// ITCM: 关键内核; DTCM: 激活/stack; AXI SRAM: 权重搬运缓冲
__attribute__((section(".dtcm_bss"), aligned(16))) int8_t act_buf[ACT_MAX];
__attribute__((section(".axi_bss"), aligned(16)))  int8_t w_tile[TILE_MAX];

推理流水(骨架)


while (1){
  // 1) 从 Ping 取一帧,异步搬权重到 w_tile
  dma_copy_flash_to_sram(layer_w_flash, w_tile, bytes);
  // 2) 前处理: MFCC/缩放 -> act_buf_in
  mfcc_q15_to_q7(ping_audio, act_buf_in);
  // 3) 等 DMA 完成,跑该层
  conv_run_int8(act_buf_in, w_tile, act_buf_out, &ctx);
  swap_buffers();
}

6.2 带 NPU 的 SoC:offload 的边界与调度

能 offload 的通常是:Conv2D/Depthwise、Pointwise(1×1)、Add、Relu/Relu6、Pool。难 offload/常在 CPU 上:reshape/concat/argmax/softmax、复杂后处理(NMS/追踪)、I/O 与预处理(颜色空间、旋转)。代价一次 offload = 一次搬运 + NPU 启动开销。小层频繁 offload 可能更慢

分块与双缓冲(NPU↔SRAM)

代码


for (int t=0; t<TILES; ++t){
  if (t+1<TILES) dma_prefetch(tile[t+1], sram_buf[next]);
  npu_submit(tile[t], sram_buf[cur]);
  if (t>0) postprocess_on_cpu(result[t-1]); // 与 NPU 计算重叠
  npu_wait(); swap(cur,next);
}

调参顺序

增 tile 尺寸到 NPU 吞吐“甜点区”,避免微小任务频繁启动;合并若干小层到一次 offload(框架支持时);把前处理(缩放/归一化)放在 CPU 并行执行。


6.3 存储布局与代码组织清单

区域 放什么 为什么
ITCM(若有) 关键卷积核例程/汇编 指令 TCM 带宽高、无等待
DTCM/SRAM 激活、scratch、环形缓冲 低时延、高带宽,峰值受限
AXI SRAM 大块权重搬运缓冲、I/O 帧 与 DMA 并行,注意仲裁
Flash/PSRAM 权重(按层/块存放) 容量大;按需搬运减抖动

对齐与打包


#define ALIGN(N) __attribute__((aligned(N)))
ALIGN(16) int8_t act0[...], act1[...]; // 双缓冲
ALIGN(64) int8_t tile_w[...];          // 方便突发/Cache line

6.4 编译与数值细节

编译
-O3 -fno-exceptions -fno-rtti
;开启目标核的 DSP/FP/NEON 相关开关;谨慎使用
-ffast-math
(与定点路径隔离)。量化:统一 对称 int8,卷积权重逐通道 scale,激活逐张量 scale;整数路径中避免往返量化溢出:累加使用 int32,卷积后再按
out_shift

out_mult
量化回 int8。对极少数“爆峰”层可局部放宽到 int16 激活。


6.5 上板自检(1 分钟版)

峰值激活 + scratch ≤ 可用 SRAM(留 ≥20%)。 前处理/推理/后处理 p95 各自可测,
L_steady = max(W, Pre+Infer+Post)+Sched
达标。 门控挡下 ≥60% 的静默帧;功耗明显下降。 CMSIS-NN/TFLM 路径全整型;无运行期
malloc/new
。 DMA/Cache 维护正确;无“偶发花数据”。 NPU offload 的 tile 尺寸与搬运重叠已生效。


7. 基线与门槛:三类典型 Workload 的对齐方法

7.1 关键词唤醒(KWS,音频 16 kHz)

输入与窗口

帧长/步长:
25 ms / 10 ms
(MFCC 40 维为常见起点)触发目标:片上前唤醒,误报可容,但漏报代价高

基线实现

门控:能量 + 过零率,挡掉静默帧特征:MFCC → 压到
8–20
维模型:DS-CNN / TCN(int8,全整型路径)

度量口径

端到端:
L = max(W, Pre+Infer+Post) + Sched
,报 p50/p95/p99峰值内存:激活 + scratch(SRAM)准确性:TPR/FPR(阈值来自验证集)功耗:mJ/触发(仪表积分或固件计量)

门槛线(建议,可按场景收紧/放宽)


p95(L) ≤ 50 ms

p99(L) ≤ 80 ms
漏报率 ≤ 1%,误报率 ≤ 1%(以小时为统计窗口)峰值激活 ≤ 可用 SRAM 的 80%触发能量 ≤ 规定上限(例如电池设备按日预算反推)

A/B 切换位

规则→线性分类→轻量 DNN,三档可配置帧长/步长、特征维度、模型宽度系数 α


7.2 手势/动作分类(IMU 6 轴,200–400 Hz)

输入与窗口

窗长:
256–512
样本,重叠
50%
起模型:1D TCN / DS-CNN-1D(int8)

基线实现

门控:|a|、角速峰-峰、步频估计特征:时域统计 + 简单带能量,或直接 1D 卷积

度量口径

端到端延迟:窗口 + 推理 + 去抖准确性:混淆矩阵、类别 F1资源:SRAM 峰值、CPU 占比

门槛线(建议)


p95(L) ≤ 150 ms
(含去抖),
p99(L) ≤ 200 ms
F1 ≥ 0.9(关键类别)漏检优先小于误检(安全相关时)

A/B 切换位

窗长与重叠比例频带特征 vs 端到端 1D 卷积量化参数(per-channel / per-tensor)


7.3 低分辨率目标“存在性”检测(96×96/128×128)

输入与目标

非分割、非定位,仅判存在/不存在摄像头 Y 灰度,缩放到
96×96

128×128

基线实现

门控:帧差像素计数、亮度直方图熵模型:MobileNet(α=0.25–0.5,int8,DW+PW)

度量口径

端到端:采集→缩放→归一化→推理→NMS/阈值占空比与温升(长时运行)误报对业务的真实代价(例如虚警推送)

门槛线(建议)


p95(L) ≤ 100 ms

p99(L) ≤ 150 ms
TPR ≥ 0.95,FPR ≤ 2%(按场景调整)平均功耗不超过平台热预算(需温升验证)


7.4 基线输出模板(CSV 字段)


case, frame_len_ms, hop_ms, f_infer_hz, L_p50_ms, L_p95_ms, L_p99_ms,
sram_peak_kb, scratch_kb, energy_mJ, TPR, FPR, F1

收敛判据

所有门槛满足;SRAM 余量 ≥ 20%端到端 p99 稳定(相邻 3 个窗口均在阈内)长时跑(≥1 小时)无复位/看门狗/丢帧异常


8. 常见坑与止损清单

8.1 忽略前处理成本

现象:推理很快,端到端仍超限止损:先减
W
/增
H
,再降分辨率/特征维;将前处理移到整型路径;使用 DMA+双缓冲流水化

8.2
im2col
临时暴涨

现象:SRAM 突破峰值或抖动加剧止损:改 direct/Winograd;用 DW+PW 替代标准卷积;逐层查询 scratch 并一次性静态分配最大值

8.3 片外访问抖动

现象:偶发长尾、能耗飙升止损:权重分块搬运到 SRAM,再计算;NPU 场景用 tile + 预取;限制并发外设对总线的争用

8.4 时间基混乱

现象:延迟统计负跳、曲线不同步止损:统一单调钟;MCU↔主机做往返偏移估计并滑动维护;录包写入偏移版本

8.5 量化不一致

现象:板上精度明显低于离线止损:全链路 int8;权重 per-channel,激活 per-tensor;标定集覆盖真实数据分布;避免在板上重复量/反量化

8.6 内存碎片与运行期分配

现象:长时运行崩溃止损:零分配策略;激活/权重/临时缓冲全部静态或对象池;避免动态字符串与日志拼接

8.7 NPU 过度 offload

现象:小层频繁启动反而更慢止损:合并小层、增大 tile;把 reshape/concat/softmax 留在 CPU 并与 NPU 计算重叠

8.8 任务调度与临界区

现象:推理被低优先级打断,p99 飙升止损:提升前处理/推理任务优先级;缩短临界区;在计算关键段禁长临界区 I/O

8.9 门控阈值设置不当

现象:漏检或无效帧过多止损:离线扫描阈值–TPR/FPR 曲线;按“低 FPR 优先”的原则设置;在现场打点回看

8.10 温升与节能策略

现象:长时降频导致延迟超限止损:限定占空比;启停策略;必要时降低频率/分辨率以稳住热预算


8.11 上板自检表(1 分钟)

三张预算表(算力/内存/延迟)均已填实 p95/p99 达标;长时跑稳定 SRAM 峰值 + scratch ≤ 可用的 80%,无运行期分配 规则门控有效拦截多数无效帧 量化路径一致;前处理、推理、后处理在同一数值域 片外访问受控;DMA/Cache 行为可预测


个人简介
把模型放上板:边缘 AI 的任务画像与可行性边界(MCU/NPU 实战视角)
作者简介:全栈研发,具备端到端系统落地能力,专注人工智能领域。
个人主页:观熵
个人邮箱:privatexxxx@163.com
座右铭:愿科技之光,不止照亮智能,也照亮人心!

专栏导航

观熵系列专栏导航:
具身智能:具身智能
国产 NPU × Android 推理优化:本专栏系统解析 Android 平台国产 AI 芯片实战路径,涵盖 NPU×NNAPI 接入、异构调度、模型缓存、推理精度、动态加载与多模型并发等关键技术,聚焦工程可落地的推理优化策略,适用于边缘 AI 开发者与系统架构师。
DeepSeek国内各行业私有化部署系列:国产大模型私有化部署解决方案
智能终端Ai探索与创新实践:深入探索 智能终端系统的硬件生态和前沿 AI 能力的深度融合!本专栏聚焦 Transformer、大模型、多模态等最新 AI 技术在 智能终端的应用,结合丰富的实战案例和性能优化策略,助力 智能终端开发者掌握国产旗舰 AI 引擎的核心技术,解锁创新应用场景。
企业级 SaaS 架构与工程实战全流程:系统性掌握从零构建、架构演进、业务模型、部署运维、安全治理到产品商业化的全流程实战能力
GitHub开源项目实战:分享GitHub上优秀开源项目,探讨实战应用与优化策略。
大模型高阶优化技术专题
AI前沿探索:从大模型进化、多模态交互、AIGC内容生成,到AI在行业中的落地应用,我们将深入剖析最前沿的AI技术,分享实用的开发经验,并探讨AI未来的发展趋势
AI开源框架实战:面向 AI 工程师的大模型框架实战指南,覆盖训练、推理、部署与评估的全链路最佳实践
计算机视觉:聚焦计算机视觉前沿技术,涵盖图像识别、目标检测、自动驾驶、医疗影像等领域的最新进展和应用案例
国产大模型部署实战:持续更新的国产开源大模型部署实战教程,覆盖从 模型选型 → 环境配置 → 本地推理 → API封装 → 高性能部署 → 多模型管理 的完整全流程
Agentic AI架构实战全流程:一站式掌握 Agentic AI 架构构建核心路径:从协议到调度,从推理到执行,完整复刻企业级多智能体系统落地方案!
云原生应用托管与大模型融合实战指南
智能数据挖掘工程实践
Kubernetes × AI工程实战
TensorFlow 全栈实战:从建模到部署:覆盖模型构建、训练优化、跨平台部署与工程交付,帮助开发者掌握从原型到上线的完整 AI 开发流程
PyTorch 全栈实战专栏: PyTorch 框架的全栈实战应用,涵盖从模型训练、优化、部署到维护的完整流程
深入理解 TensorRT:深入解析 TensorRT 的核心机制与部署实践,助力构建高性能 AI 推理系统
Megatron-LM 实战笔记:聚焦于 Megatron-LM 框架的实战应用,涵盖从预训练、微调到部署的全流程
AI Agent:系统学习并亲手构建一个完整的 AI Agent 系统,从基础理论、算法实战、框架应用,到私有部署、多端集成
DeepSeek 实战与解析:聚焦 DeepSeek 系列模型原理解析与实战应用,涵盖部署、推理、微调与多场景集成,助你高效上手国产大模型
端侧大模型:聚焦大模型在移动设备上的部署与优化,探索端侧智能的实现路径
行业大模型 · 数据全流程指南:大模型预训练数据的设计、采集、清洗与合规治理,聚焦行业场景,从需求定义到数据闭环,帮助您构建专属的智能数据基座
机器人研发全栈进阶指南:从ROS到AI智能控制:机器人系统架构、感知建图、路径规划、控制系统、AI智能决策、系统集成等核心能力模块
人工智能下的网络安全:通过实战案例和系统化方法,帮助开发者和安全工程师识别风险、构建防御机制,确保 AI 系统的稳定与安全
智能 DevOps 工厂:AI 驱动的持续交付实践:构建以 AI 为核心的智能 DevOps 平台,涵盖从 CI/CD 流水线、AIOps、MLOps 到 DevSecOps 的全流程实践。
C++学习笔记?:聚焦于现代 C++ 编程的核心概念与实践,涵盖 STL 源码剖析、内存管理、模板元编程等关键技术
AI × Quant 系统化落地实战:从数据、策略到实盘,打造全栈智能量化交易系统
大模型运营专家的Prompt修炼之路:本专栏聚焦开发 / 测试人员的实际转型路径,基于 OpenAI、DeepSeek、抖音等真实资料,拆解 从入门到专业落地的关键主题,涵盖 Prompt 编写范式、结构输出控制、模型行为评估、系统接入与 DevOps 管理。每一篇都不讲概念空话,只做实战经验沉淀,让你一步步成为真正的模型运营专家。


🌟 如果本文对你有帮助,欢迎三连支持!

👍 点个赞,给我一些反馈动力
⭐ 收藏起来,方便之后复习查阅
🔔 关注我,后续还有更多实战内容持续更新

© 版权声明

相关文章

暂无评论

none
暂无评论...