2、数据流基础:嵌入式系统中的数据流模型,DAG(有向无环图)与流水线。

好,咱们进入正题。这一章聊的是数据流,说白了就是数据在芯片里怎么“跑”的。你想想看,一个BEV模型,从摄像头拿到图像,到最终输出障碍物位置,中间要经过几十甚至上百个算子。这些算子怎么组织、怎么调度,直接决定了你的系统能不能跑在30帧甚至更高。

我个人习惯,拿到一个模型,第一件事不是看精度,而是画数据流图。为什么?因为数据流决定了你的瓶颈在哪。我见过太多团队,模型精度刷得飞起,一上板子就卡成PPT,问题就出在数据流没理清楚。

2.1 从“串行”到“并行”:数据流模型的演进

最早期的嵌入式系统,数据流很简单。传感器采集 -> CPU处理 -> 输出。一条直线走到底。这叫串行模型。但BEV模型不一样,它天生就是多路的。比如前视、环视、激光雷达,各路数据进来,先各自处理,再融合,最后输出。

串行模型的问题很明显:。一个算子跑完,下一个才能开始。如果某个算子特别耗时,整个系统都得等它。我在项目中遇到过,一个BEV模型里有个“可变形注意力”算子,在GPU上跑得飞快,但到了嵌入式平台,单次推理就要80毫秒。如果串行跑,整个pipeline就被它拖死了。

所以,我们需要并行。并行有两种基本形式:

  • 数据并行:同一份数据,分给多个处理单元同时处理。比如把一张图切成四块,四个核同时算。
  • 任务并行:不同的算子,在不同的数据上同时执行。比如A核在算特征提取,B核在算后处理。

但并行不是你想并就能并的。你得搞清楚算子之间的依赖关系。这就引出了我们的核心工具——DAG。

2.2 DAG:数据流的地图

DAG,有向无环图。名字听着唬人,其实就是一张图,节点是算子,边是数据依赖。为什么强调“无环”?因为如果有环,数据就会死循环,系统就崩了。

我画DAG有个习惯:从上到下,从左到右。输入在最上面,输出在最下面。中间每个节点,只依赖它上面的节点。这样一眼就能看出哪些节点可以并行。

核心原则:DAG中,没有依赖关系的节点,理论上可以并行执行。

举个例子,一个简化的BEV模型DAG:

输入: 6路环视图像
├── 节点A: 图像预处理 (缩放、归一化)
├── 节点B: 特征提取 (Backbone)
├── 节点C: 视角转换 (LSS / Transformer)
├── 节点D: BEV特征融合
├── 节点E: 检测头 (Head)
└── 输出: 障碍物列表

你看,节点A和节点B是串行的,因为B依赖A的输出。但节点C和节点D呢?如果C是视角转换,D是特征融合,那D必须等C的结果。但如果模型设计得好,C和E(检测头)可能没有直接依赖,那就可以并行。

嗯,这里要注意:DAG只是逻辑模型,不是物理实现。你画出来的DAG再漂亮,如果硬件不支持并行,那也是白搭。所以,下一步就是把DAG映射到硬件流水线上。

2.3 流水线:让数据“流”起来

流水线的思想,其实来自工厂。一条生产线,每个工位只做一件事。产品流过一个工位,就完成一道工序。嵌入式系统里的流水线也一样:每个处理单元(CPU核、NPU、DSP)就是一个工位,数据流过这些工位,逐步完成推理。

流水线的核心指标是吞吐量。不是单次推理有多快,而是单位时间内能处理多少帧。我见过一个团队,单帧推理优化到了50毫秒,但流水线没做好,实际帧率只有12帧。为什么?因为流水线有“气泡”。

避坑指南:我曾经踩过一个坑,把预处理和推理放在同一个核上跑。结果预处理占用了推理的时间,导致流水线断流。后来我把预处理单独放到一个CPU核上,推理放到NPU上,帧率直接翻倍。

设计流水线,有几个关键点:

  1. 平衡各阶段负载:流水线的速度,取决于最慢的那个阶段。如果特征提取要80ms,后处理只要10ms,那后处理大部分时间都在空等。这时候就要考虑把特征提取拆成更细的步骤,或者用更快的硬件。
  2. 减少数据搬运:嵌入式系统里,数据搬运比计算还贵。从DDR搬到SRAM,再从SRAM搬到NPU,每一步都有延迟。我建议尽量让数据在本地处理完,不要来回倒腾。
  3. 使用双缓冲:这是流水线的经典技巧。一块缓冲区在计算,另一块在加载下一帧数据。计算完,直接切换。这样计算和加载可以完全重叠。

下面是一个典型的双缓冲流水线时序表:

时间片 CPU (预处理) NPU (推理) CPU (后处理)
T1 加载帧1 空闲 空闲
T2 预处理帧1 空闲 空闲
T3 加载帧2 推理帧1 空闲
T4 预处理帧2 推理帧2 后处理帧1
T5 加载帧3 推理帧3 后处理帧2

你看,从T3开始,三个处理单元都在干活,没有空闲。这就是流水线的理想状态。

2.4 从DAG到流水线:一个实战案例

光说不练假把式。我拿一个实际项目来说。去年我做了一个BEV模型,部署在Jetson Orin上。模型结构是这样的:

  • 6路图像,每路经过一个轻量级Backbone(MobileNetV3)
  • 然后通过LSS(Lift-Splat-Shoot)转换到BEV空间
  • 最后用一个轻量级检测头输出结果

一开始,我按串行方式跑:6路Backbone串行,然后LSS,然后Head。结果帧率只有8帧。我画了DAG一看,问题很明显:6路Backbone之间没有依赖关系,完全可以并行!

于是我把流水线改成这样:

  1. 用6个CPU线程,同时处理6路图像的预处理和Backbone推理
  2. 等6路都完成后,统一交给NPU做LSS和Head
  3. NPU推理的同时,CPU开始加载下一帧的6路图像

改完之后,帧率从8帧提升到了22帧。嗯,这里要注意:并行不是越多越好。6路并行,如果CPU只有4个核,那就会产生线程切换开销。我后来把6路合并成3路(每路处理2张图),帧率反而更高了。

个人经验:设计流水线时,一定要考虑硬件的实际能力。不要被理论峰值迷惑。我建议先画DAG,再根据硬件资源(核数、内存带宽、NPU算力)做裁剪。有时候,减少并行度反而能提升整体吞吐量。

2.5 总结:数据流优化的三个层次

好了,这一章的内容差不多就这些。我总结一下数据流优化的三个层次:

  • 逻辑层:画DAG,理清依赖关系,找出可并行的节点。
  • 架构层:设计流水线,平衡各阶段负载,减少数据搬运。
  • 实现层:用双缓冲、多线程、异步调用等技巧,把流水线跑起来。

下一章,我们会深入具体的算子优化。但在此之前,我建议你把模型的DAG画出来。不用画得太细,把主要模块和它们之间的数据依赖标清楚就行。你会发现,很多性能问题,光看DAG就能发现。

记住:数据流理顺了,优化就成功了一半