3. 推理引擎基础

自动驾驶推理引擎并不是“把模型跑起来”这么简单。它要在毫秒级时延约束下,把来自摄像头、激光雷达、毫米波雷达的输入张量,经过预处理、算子计算、后处理,稳定地输出检测框、分割掩码或轨迹预测。理解推理引擎,关键是理解一条从模型文件到硬件执行的完整链路:模型推理流程决定数据怎么走,计算图决定计算依赖,算子决定计算粒度,运行时决定资源如何分配,调度器与执行引擎决定谁先算、谁后算、在哪里算。

一、模型推理流程:从输入到输出的五段式链路

一个典型的自动驾驶感知模型推理流程可以拆成五个阶段:输入预处理 → 图加载与优化 → 算子调度与执行 → 输出后处理 → 结果发布。预处理包括归一化、颜色空间转换、resize、padding;图加载阶段会完成算子融合、常量折叠、内存复用规划;执行阶段由运行时分配显存/内存,调度器按依赖关系派发算子;后处理则完成 NMS、解码、阈值过滤等操作。

以 BEV 感知模型为例,输入可能是 6 路 1920×1080 图像,经过 resize 到 640×640 后送入 backbone,再经过 neck、BEV 变换、检测头,最终输出 200 个候选框。整个链路中,真正在 GPU 上做矩阵乘的时间可能只占 40%,其余时间消耗在预处理、内存拷贝和后处理上。因此,性能优化必须看全链路,而不是只盯着算子。

核心认知:推理引擎的性能瓶颈往往不在“算”,而在“搬”和“等”。数据搬运、同步等待、内存分配、算子启动开销,都是自动驾驶场景下不可忽视的成本。

二、计算图:推理引擎的“程序表示”

计算图是推理引擎对模型的中间表示。它由节点(算子)和边(张量)组成,描述了数据依赖关系。计算图分为静态图和动态图:静态图在编译期确定形状和内存,适合部署;动态图在运行期构建,灵活但开销大。自动驾驶推理引擎通常采用静态图,并在图级别做优化。

常见的图优化包括:算子融合(如 Conv+BN+ReLU 融合为一个算子)、常量折叠(把编译期可计算的子图提前算掉)、死代码消除、内存复用(生命周期不重叠的张量共享同一块内存)。这些优化直接决定推理时延和显存占用。

推理引擎核心组件与数据流 模型文件 ONNX / TensorRT 计算图 节点 + 张量边 图优化器 融合 / 折叠 / 复用 运行时 内存 / 流 / 设备 调度器 依赖排序 / 派发 执行引擎 算子内核 / 并行 硬件后端 GPU / NPU / DSP 数据流:输入张量 → 预处理 → 算子执行 → 后处理 → 输出结果 控制流:调度器按依赖关系派发算子,执行引擎调用硬件内核,运行时管理内存与同步

三、算子、运行时与调度器:谁在真正干活

算子是计算图的最小执行单元。在自动驾驶模型中,常见算子包括 Convolution、MatMul、Add、Mul、Sigmoid、Softmax、Resize、NMS、TopK 等。不同硬件后端对同一算子的实现差异很大:GPU 上可能用 cuDNN 或 TensorRT 内核,NPU 上则用厂商私有指令。推理引擎需要为每个算子选择或生成高效内核。

运行时负责资源管理:显存分配、内存池、CUDA Stream、事件同步、设备上下文。运行时决定了算子能否异步执行、能否重叠计算与拷贝。一个设计良好的运行时会使用内存池避免频繁 malloc/free,使用多流实现预处理与推理并行。

调度器负责按计算图的依赖关系决定算子执行顺序。它需要处理:就绪队列、依赖计数、优先级、流分配。在自动驾驶中,调度器还要考虑多模型并发、多帧流水线、实时性优先级。例如,障碍物检测模型比车道线分割模型优先级更高,调度器应优先保证前者时延。

执行引擎是算子的实际执行者。它接收调度器派发的算子任务,调用对应硬件内核,管理线程块、网格、共享内存等底层资源。执行引擎的效率直接决定算子的实际吞吐。

组件 核心职责 关键优化点 常见瓶颈
计算图 表达模型结构与依赖 算子融合、常量折叠、内存复用 图过大、冗余节点
算子 最小计算单元 内核选择、自动调优、量化 内核启动开销、访存密集
运行时 资源分配与同步 内存池、多流、异步拷贝 频繁分配、同步等待
调度器 依赖排序与任务派发 优先级、流水线、并发调度 调度开销、负载不均
执行引擎 调用硬件内核执行 并行度、共享内存、指令优化 占用率低、寄存器溢出

四、案例与场景:一次 BEV 模型推理的组件协作

假设我们部署一个 BEVFormer 类模型,输入 6 路图像,输出 BEV 特征图。推理引擎的工作流程如下:

  1. 图加载:读取 ONNX 模型,解析为计算图,识别出 1200 个节点。
  2. 图优化:融合 Conv+BN+ReLU,折叠常量,将节点数降到 800 个,显存占用降低 30%。
  3. 运行时初始化:创建 CUDA Stream,分配输入输出张量内存池,预分配中间激活内存。
  4. 调度:调度器按拓扑排序,将无依赖的算子放入就绪队列,优先派发 backbone 算子。
  5. 执行:执行引擎调用 TensorRT 内核,在 GPU 上并行执行卷积和矩阵乘。
  6. 后处理:输出 BEV 特征后,执行解码、NMS、阈值过滤,生成最终检测结果。
# 伪代码:推理引擎一次前向的核心调度逻辑
for op in topological_order(graph):
    if all_deps_ready(op):
        stream = select_stream(op)
        kernel = runtime.get_kernel(op, device)
        engine.launch(kernel, op.inputs, op.outputs, stream)
    else:
        wait_for_dependencies(op)

# 多流并行示例
stream_preprocess = runtime.create_stream()
stream_infer = runtime.create_stream()
stream_postprocess = runtime.create_stream()

with stream_preprocess:
    preprocess(image)
with stream_infer:
    model.forward(tensor)
with stream_postprocess:
    postprocess(output)

实操建议:排查推理性能问题时,先看时间分布:预处理、推理、后处理各占多少。再用 profiling 工具看算子耗时排名。最后检查是否存在频繁内存分配、同步等待、流竞争。多数情况下,优化内存复用和算子融合的收益,比换硬件更直接。

五、误区与复盘:不要只盯着算子优化

很多工程师一上来就优化卷积算子,结果发现端到端时延只降了 5%。原因在于:推理引擎是一个系统,瓶颈可能在调度、内存、同步、后处理。常见误区包括:

  • 误区一:认为算子越快,整体越快。实际上,算子启动开销和调度开销可能占主导。
  • 误区二:忽略预处理和后处理。在自动驾驶中,这两部分可能占 30% 以上时延。
  • 误区三:不区分静态图和动态图。动态图在部署时可能带来额外解释开销。
  • 误区四:忽视内存复用。中间激活内存频繁分配会导致显存碎片和同步等待。

复盘时,建议按“图优化 → 运行时配置 → 调度策略 → 算子内核 → 后处理”的顺序逐层排查。每一层都有明确的观测指标:图节点数、内存分配次数、流利用率、算子耗时、后处理耗时。只有建立完整的性能视图,才能定位真正的瓶颈。

注意:自动驾驶推理引擎的优化目标不是“最快”,而是“最稳”。时延抖动比平均时延更危险。调度器必须保证关键任务在截止时间内完成,运行时必须避免不可预测的内存分配和同步等待。

总结来说,推理引擎的核心组件各司其职:计算图定义“算什么”,算子定义“怎么算”,运行时定义“在哪算、用什么算”,调度器定义“什么时候算”,执行引擎定义“实际怎么执行”。理解它们之间的协作关系,是后续性能优化的基础。