7AA7 博客

边缘计算实践:设备侧推理与数据聚合

码同客 UID:88
2026-10-10 15:30
29 2 0

把计算放到设备侧,出发点是”数据传不动”或者”数据不该传”。想清楚到底是哪一条在驱动,才能决定该放多少算力在端上。

为什么要算在设备侧

带宽:一路摄像头或者振动传感器的原始数据量级远大于蜂窝网络的实际可用上行带宽,全量上传既不现实也不经济。在端上做一帧推理,只把结果和少量特征传上去,流量能降几个数量级。

时延:云端往返的时延量级在几十到几百毫秒,且抖动大。闭环控制、联动响应这类场景等不起,本地决策必须在本地完成。

隐私:图像、声音这类数据本身携带敏感信息。在端上处理完只上传结构化结果,数据采集面显著缩小,合规压力也随之降低。

断网可用:网络不是永远在的。设备如果只是云端的一个传感器终端,断网期间就是瞎的;有本地推理能力,断网期间的核心功能仍然可用。

代价是端侧算力受限、部署和更新变复杂、排查链路变长。

算力与内存的现实约束

MCU 上的算力预算和云端完全不是一个量级。粗略地说,主频在几百 MHz 量级、SRAM 在几百 KB 量级、flash 在几 MB 量级是这类设备的常见配置区间(具体以所用芯片手册为准)。有硬件加速单元的器件在乘加密集运算上能有明显提升,但内存往往仍是瓶颈。

内存的三块占用:

  • 模型权重:flash 里存,运行时要么整体搬进 RAM,要么按层加载
  • 激活值(中间张量):这是最容易被低估的一块,峰值取决于网络中最大的特征图尺寸
  • 工作区(arena / tensor arena):推理框架通常要求一块连续内存做内存池

实际经验是激活值比权重更吃内存:层数不多但特征图分辨率高的网络,可能比层数多但分辨率低的网络更费 RAM。

没有 FPU 的器件跑浮点模型非常慢,通常必须量化。有 FPU 的也要考虑 double 是软件模拟的,代码里一个不经意的 0.1 常量就可能把整段代码拉成浮点。

模型量化的原理与代价

量化的核心是把浮点权重映射到整数。最常见的对称线性量化:

real_value = scale × (int8_value - zero_point)

scale 是缩放因子,zero_point 是零点偏移。int8 量化把 32 位浮点压成 8 位整数,模型体积降到约 1/4,且整数乘加在大多数 MCU 上有专门的指令(比如 SIMD 的乘累加),速度提升往往是几倍量级。

量化的精度损失来自两点:表示范围被离散成 256 个台阶,落在台阶之间的值只能就近取整;逐层量化误差会累积传播。分类任务掉几个百分点通常可以接受,回归任务或输出值域敏感的任务可能直接不可用。

减轻损失的办法:

逐通道量化(per-channel):每个输出通道一套 scale,比整层一套 scale 精确得多,对深度可分离卷积这类结构收益明显。

量化感知训练(QAT):训练时在前向传播里插入量化节点,让模型自己适应量化误差。比训练后量化(PTQ)精度好,代价是要改训练流程。

校准集:PTQ 需要一组有代表性的数据统计激活值分布来定 scale。校准集不具代表性,量化后的模型在真实数据上就会崩。

混合精度:对量化误差特别敏感的层保留浮点或 16 位,其余层量化,代价是复杂度上升。

推理时的内存布局

推理框架在 MCU 上的典型做法是给一块连续内存池(arena),由框架内部的内存规划器为每层输入输出张量分配池内偏移。这个规划器会做张量生命周期分析:某个张量在最后一层用完之后,它占的空间就可以给别的张量复用。

几个实践要点:

  • arena 大小要实测。框架通常提供接口报告所需最小 arena 尺寸,先用大值跑通再按报告值收敛。给不够会分配失败,且报错信息往往不明确。
  • arena 最好静态分配成全局对齐数组,不要放到栈上。对齐要求通常是 16 字节,用 __attribute__((aligned(16))) 保证。
  • 输入张量的预处理要在 arena 之外做。归一化、量化这些操作可以就地完成,避免再来一份缓冲。
  • 模型放 flash 做 XIP 还是拷到 RAM,取决于 flash 访问速度和是否需要运行时改写。带缓存的 flash 直接 XIP 通常够用。

数据聚合上报策略

定时聚合:固定周期打包上报。行为可预测,便于服务端做窗口统计,实现最简单。缺点是不管有没有变化都发,流量固定。

变化上报:值变了才发。省流量,但需要一个”变化多少算变化”的死区阈值。阈值太小,噪声会导致频繁上报;阈值太大,真实的小变化被吞掉。通常会配一个最小上报间隔兜底,防止噪声把上报频率打满。

阈值触发:只在越过关键阈值时上报,比如温度超过上限。适合告警类场景,但设备平时是”沉默”的,无法判断它是不是还活着。通常要配一个低频心跳。

三种策略的取舍本质是流量、电量、数据完整性的三角。电池设备往往选”变化上报 + 心跳”,插电设备选”定时聚合”。很多系统做成可配置:默认定时聚合,服务端下发指令临时切高频采集做诊断。

聚合还要注意统计量的选择。上传平均值会丢失峰值和方差,业务关心峰值就同时上报窗口内最大最小值或直方图。聚合一旦做了,丢掉的信息就回不来了。

断网缓存与补传

网络中断期间的数据不能丢。本地缓存的常用结构是环形缓冲:

typedef struct {
    uint32_t head, tail, count, cap;
    record_t *buf;
} ring_t;

int ring_push(ring_t *r, const record_t *rec) {
    if (r->count == r->cap) {
        r->tail = (r->tail + 1) % r->cap;   /* 覆盖最旧,避免写入阻塞 */
        r->count--;
    }
    r->buf[r->head] = *rec;
    r->head = (r->head + 1) % r->cap;
    r->count++;
    return 0;
}

缓冲区满了之后是覆盖最旧还是丢弃最新,取决于业务:实时监测类的关心最新数据,覆盖最旧;审计类的关心完整历史,丢弃最新并上报缓存溢出。

补传时要注意三点:

去重:每条记录带一个单调递增的序号,服务端按序号去重。补传和网络重试都可能造成重复投递。

顺序:补传的历史数据与实时数据会交错,服务端要按序号或时间戳排序,不能假定到达顺序等于发生顺序。

限速:如果缓存了几千条,重连瞬间全部涌出去会冲垮网络栈和后端。按批次限速补传,比如每批几十条,批间留间隔。

持久化缓存要写 flash 就要考虑擦写次数和掉电安全。每写一条擦一次的写法会迅速耗尽 flash 寿命,通常攒够一个扇区再写,接受掉电时丢失最后几条。

模型更新的版本管理

模型和固件一样需要 OTA,但它有自己的复杂度:

版本绑定:模型版本和应用代码版本之间有兼容性约束。模型输入输出张量的形状、归一化参数变了,处理它的代码就得跟着变。必须维护一个兼容矩阵,不能只靠一个版本号判断。

原子性:模型文件写入中途断电,会留下半个文件。要写到一个暂存区,校验通过后再切换指针。

回滚:新模型上线后若推理失败率或异常指标恶化,要能退回上一个已知可用版本。触发条件要写成可判定的规则,而不是人工拍板。

灰度:先推一小批设备,观察一段再扩大范围。同一批设备里最好包含不同硬件版本,避免只在某一种板子上验证。

模型与固件分开更新会让版本组合爆炸(M 个固件版本 × N 个模型版本)。约束组合数量的方法是把兼容范围写进模型元数据里,由设备自己判断是否接受这次更新。

1.3k浏览量
29赞
2评论
2参与用户
8 分钟阅读时间

评论(2)

  • 码同客 · 2026-10-10 18:02

    之前一直半懂,现在算是串起来了。

    回复
  • 林见川 · 2026-10-11 03:03

    不建议新手直接照抄,有些前提没写清楚(不是黑,是真的容易翻车)。

    回复