3、TinyML推理调用场景:典型实时任务(传感器采集、推理、控制输出)中的并发需求与挑战

TinyML系统通常部署在资源极度受限的微控制器(MCU)上,却需要完成从物理世界感知到决策输出的完整闭环。一个典型的实时任务链路包含三个环节:传感器数据采集、神经网络推理、控制信号输出。这三个环节在时间轴上紧密耦合,且往往与通信、日志、显示等其他任务共享同一颗单核CPU。理解这一场景中的并发需求,是设计线程安全推理接口的前提。下图展示了典型TinyML实时任务链路的并发模型与资源竞争关系。

传感器采集任务 IMU / 麦克风 / 摄像头 周期: 10-100ms 推理任务 TFLite / 自定义RTOS 耗时: 5-200ms 控制输出任务 PWM / GPIO / 电机 周期: 1-20ms 共享缓冲区 推理结果 共享资源与竞争区域 全局推理上下文 权重/中间张量 DMA缓冲区 采集数据暂存 输出寄存器 控制参数 中断/信号量 优先级反转风险 数据覆盖 读写竞争 输出抖动 死锁/饿死

上图揭示了TinyML实时系统中并发问题的本质:三个任务在时间上重叠,在数据上共享,在资源上竞争。传感器采集任务以固定周期将数据写入缓冲区,推理任务需要读取这些数据进行计算,控制输出任务则依赖推理结果产生物理动作。任何一个环节的阻塞或数据不一致,都会导致整个控制链路的失效。

3.1 核心原理:单核并发与任务调度的基本模型

TinyML设备绝大多数采用单核MCU(如Cortex-M系列),这意味着所谓的“并发”并非真正意义上的并行执行,而是通过操作系统(RTOS)的时间片轮转或优先级抢占来实现任务间的交替运行。这种模型下,推理接口的线程安全面临三个层面的挑战:

  • 任务切换时机不可控:RTOS可能在任意指令边界触发上下文切换。如果推理函数正在修改中间张量,此时被更高优先级任务打断,再次恢复时数据可能已被破坏。
  • 中断与任务的竞争:传感器数据通常通过DMA或外部中断触发采集。中断服务程序(ISR)可以随时抢占任务上下文,若ISR与推理任务共享缓冲区,则必须考虑原子操作。
  • 资源有限性:MCU的RAM通常只有几十KB,无法像Linux那样为每个任务分配独立的内存池。推理所需的权重、激活缓冲区、输出张量往往只能全局共享一份。

理解这一模型的关键在于:线程安全不是“避免同时访问”,而是“在访问被中断时保证数据一致性”。TinyML推理接口的设计必须围绕“临界区保护”和“数据版本管理”两个核心展开。

3.2 判断方法:识别并发风险的四个维度

在实际项目中,判断推理调用是否存在并发风险,可以从以下四个维度进行系统检查。下表总结了各维度的检查要点与典型风险信号。

检查维度 关键问题 典型风险信号
任务优先级 推理任务与采集/输出任务的优先级关系是否明确? 高优先级任务频繁打断推理,导致推理耗时超过任务周期
数据共享 输入缓冲区、输出缓冲区是否被多个任务引用? 采集任务写入新数据时,推理任务仍在读取旧数据
可重入性 推理函数是否被设计为可重入(无全局状态依赖)? 推理函数内部使用静态局部变量保存中间结果
中断延迟 ISR中是否调用了推理接口或访问了推理数据? ISR与推理任务同时操作同一全局变量

一个实用的判断流程是:首先绘制任务时序图,标注每个任务的周期、执行时间和截止时间;然后标记所有跨任务访问的全局变量和缓冲区;最后检查每个共享数据是否有明确的“生产者-消费者”关系,以及是否存在“多生产者-单消费者”或“单生产者-多消费者”的复杂模式。如果发现任何共享数据没有对应的互斥机制,即存在并发风险。

3.3 案例拆解:智能电机振动监测系统的并发设计

以一个工业电机振动监测系统为例,该系统使用STM32F4系列MCU,运行FreeRTOS,通过I2S接口采集加速度计数据,使用TFLite Micro进行异常检测,并通过PWM输出报警信号。系统任务配置如下:

  • 采集任务:优先级3,周期10ms,每次采集256个16位样本(512字节),写入DMA双缓冲。
  • 推理任务:优先级2,周期50ms,读取最近5帧数据(2560字节),执行CNN推理,耗时约35ms。
  • 控制任务:优先级4,周期5ms,根据推理结果更新PWM占空比。

初始设计中,采集任务和推理任务直接共享一个全局数组sensor_data[2560],推理任务读取时采集任务可能正在写入新数据,导致推理输入出现“半新半旧”的混合帧。系统表现为偶发误报警,且故障难以复现。

分析过程:通过逻辑分析仪抓取任务切换时序,发现采集任务在推理任务读取数据期间发生了两次写入操作。进一步检查代码,发现采集任务使用memcpy写入数据,而推理任务直接通过指针访问,两者之间没有任何同步机制。

解决方案采用“双缓冲+信号量”机制:

// 双缓冲定义
static int16_t buffer_a[2560];
static int16_t buffer_b[2560];
static int16_t *active_buf = buffer_a;
static int16_t *ready_buf = NULL;
static SemaphoreHandle_t data_ready_sem;

// 采集任务(ISR或高优先级任务)
void采集任务(void) {
    // DMA完成中断中切换缓冲区
    int16_t *new_buf = (active_buf == buffer_a) ? buffer_b : buffer_a;
    memcpy(new_buf, dma_buffer, 2560 * 2);
    active_buf = new_buf;
    xSemaphoreGive(data_ready_sem);  // 通知推理任务
}

// 推理任务
void推理任务(void) {
    int16_t *input;
    xSemaphoreTake(data_ready_sem, portMAX_DELAY);
    input = active_buf;  // 获取当前完整帧
    // 执行推理,期间采集任务会写入另一个缓冲区
    inference_result = run_tflite(input);
    // 更新控制输出
    update_pwm(inference_result);
}

改进后,采集任务始终写入非活动缓冲区,推理任务读取活动缓冲区,通过信号量保证“读取时缓冲区内容完整”。系统连续运行72小时,误报警次数从平均每小时2.3次降为0次。该案例的关键经验是:在TinyML中,数据一致性比计算速度更重要,双缓冲的RAM开销(约5KB)远小于错误推理带来的控制风险。

3.4 常见误区与总结复盘

在TinyML推理接口的并发设计中,开发者常陷入以下误区:

误区一:认为关中断可以解决所有问题。关中断确实能保护临界区,但会导致中断延迟增加,对于实时性要求高的传感器采集(如电机控制中的电流采样)可能造成数据丢失。正确做法是尽量缩短临界区,或使用无锁数据结构。
误区二:忽视推理函数的可重入性。TFLite Micro的Interpreter对象默认不是线程安全的。如果多个任务同时调用Invoke(),会导致权重数据错乱。必须确保同一时间只有一个任务执行推理,或为每个任务创建独立的Interpreter实例(但RAM开销巨大)。
误区三:将互斥锁粒度设计得过粗。如果对整个推理过程加锁,高优先级的控制任务会被阻塞数十毫秒,导致输出抖动。更优的做法是只对“输入数据拷贝”和“输出结果读取”这两个短操作加锁,推理计算本身不加锁(通过独占任务实现)。

总结本章核心要点:TinyML推理调用的并发控制需要从任务模型、数据流、同步机制三个层面综合考虑。设计时应遵循以下原则:

  1. 明确任务优先级:推理任务通常设为中等优先级,避免阻塞高实时性任务。
  2. 使用双缓冲或环形缓冲区:解耦采集与推理的数据依赖,减少锁竞争。
  3. 最小化临界区:只保护共享数据的读写瞬间,不保护整个推理过程。
  4. 考虑中断安全:ISR中只能使用portYIELD_FROM_ISR等中断安全API,不能直接调用阻塞型互斥锁。