本项目采用 ONNX Runtime 作为强化学习策略网络的底层推理引擎,负责将 PyTorch 训练得到的策略模型(.onnx)部署到机器人端进行实时推理。本页面向具备 C++ 与深度学习部署基础的开发者,系统讲解 ONNX Runtime 的集成架构、张量管理、多策略运行时支持以及性能优化手段,帮助你理解推理管线如何与观测组装、动作执行衔接。
Sources: inference_node.hpp, inference_node.cpp
架构定位与核心抽象
推理引擎在整个系统中的定位是「策略执行层」:接收由观测管理器组装好的浮点向量,调用 ONNX Runtime 执行前向传播,输出关节目标位置。为了保持多策略热切换能力,代码引入了两级核心抽象:ModelContext 封装纯 ONNX 运行时资源,PolicyRuntime 在其之上叠加策略配置、观测布局与帧堆叠状态。
classDiagram
class ModelContext {
+unique_ptr~Ort::Session~ session
+unique_ptr~Ort::MemoryInfo~ memory_info
+unique_ptr~Ort::Value~ input_tensor
+unique_ptr~Ort::Value~ output_tensor
+vector~string~ input_names
+vector~string~ output_names
+vector~float~ input_buffer
+vector~float~ output_buffer
+vector~int64_t~ input_shape
+vector~int64_t~ output_shape
}
class PolicyRuntime {
+string name
+string model_path
+vector~ObsSourceSpec~ obs_layout
+vector~float~ obs
+int frame_stack
+ObsStackOrder stack_order
+unique_ptr~ModelContext~ ctx
+shared_ptr~MotionLoader~ motion_loader
}
class InferenceNode {
+unique_ptr~Ort::Env~ env_
+vector~PolicyRuntime~ policies_
+int active_policy_idx_
+setup_model(ctx, path, input_size)
+inference()
}
InferenceNode "1" *-- "many" PolicyRuntime : manages
PolicyRuntime "1" *-- "1" ModelContext : owns
ModelContext 完全隔离了 ONNX C++ API 的所有句柄,包括 Ort::Session、Ort::MemoryInfo 与张量对象,确保每个策略模型拥有独立的输入输出缓冲区,避免多策略切换时的资源竞争。PolicyRuntime 则保存了观测布局、frame_stack 配置以及动作参考加载器,将「模型怎么跑」与「策略需要什么观测」解耦。这种分层设计使得新增一个策略仅需在 YAML 中追加配置项,系统会在构造函数中自动为其创建独立的 ModelContext。
Sources: inference_node.hpp, inference_node.cpp
构建系统集成与跨平台支持
项目没有依赖系统包管理器中的 ONNX Runtime,而是在 thirdparty/ 目录下预置了 x64 与 aarch64 两个平台的官方预编译库(版本 1.21.0),通过 CMake 根据目标架构自动选择链接路径。
| 目标架构 | 预编译库路径 | 说明 |
|---|---|---|
x86_64 |
thirdparty/onnxruntime-linux-x64-1.21.0 |
桌面仿真 / x86 工控机 |
aarch64 |
thirdparty/onnxruntime-linux-aarch64-1.21.0 |
Jetson / ARM 嵌入式平台 |
在 CMakeLists.txt 中,通过 CMAKE_SYSTEM_PROCESSOR 进行条件分支,将对应路径加入包含目录与链接库,并在安装阶段把 .so 文件复制到 lib 目录,确保部署时动态链接正确解析。推理节点可执行文件通过 -Wl,--no-as-needed 标志强制链接 libonnxruntime.so,避免符号延迟加载导致的运行时异常。
Sources: CMakeLists.txt, CMakeLists.txt
模型加载与生命周期管理
所有策略模型在 InferenceNode 构造阶段一次性加载完成。setup_model() 方法负责完成从 ONNX 文件到可运行会话的完整初始化,其核心流程可分为五步:
- 创建 SessionOptions:关闭每会话独立线程池、启用 CPU 内存池与内存模式、开启最高级别图优化(
ORT_ENABLE_ALL)。 - 实例化 Session:通过
Ort::Session加载.onnx文件。 - 输入校验:强制要求模型仅有 1 个输入,并校验 ONNX 输入张量尺寸是否与配置计算出的
obs_num * frame_stack + extra_obs_num完全一致,尺寸不匹配会立即抛出异常终止启动。 - 预分配缓冲区:根据输入形状和关节数量分别分配
input_buffer和output_buffer。 - 绑定张量:利用
Ort::Value::CreateTensor将预分配缓冲区包装为 ONNX Tensor,后续推理循环直接复用,无需每次重新分配。
这种「启动时加载、运行时复用」的策略消除了实时循环中的内存分配开销,是满足硬实时约束的关键设计。
Sources: inference_node.cpp
张量管理与零拷贝推理
推理循环对延迟极度敏感,因此项目采用零拷贝(Zero-Copy)张量绑定机制。input_tensor 与 output_tensor 在初始化时通过 Ort::Value::CreateTensor 直接指向 ModelContext 中 std::vector<float> 的底层数据指针,生命周期内永不重新创建。
flowchart LR
A[obs_manager<br/>组装观测] -->|std::copy| B[input_buffer]
B -->|Ort::Value::CreateTensor<br/>零拷贝引用| C[ONNX Runtime<br/>Session::Run]
C -->|写入| D[output_buffer]
D -->|action_scale + default_angle| E[RobotInterface<br/>apply_action]
在 inference() 线程中,每次循环仅执行:
- 将最新观测通过
std::copy/std::move写入input_buffer; - 调用
session->Run(..., input_tensor.get(), ..., output_tensor.get(), ...); - 从
output_buffer读取动作,经action_scale_缩放与默认角度偏移后送入电机。
整个路径没有额外的 malloc 或 memcpy 产生,输入输出张量与缓冲区之间始终维持指针引用关系。
Sources: inference_node.cpp, inference_node.cpp
实时推理循环与线程配置
推理运行在独立的 inference 线程中,通过 SCHED_FIFO 设置为实时优先级 70,周期由 dt * decimation 决定(例如 dt=0.004,decimation=5 时周期为 20 ms)。线程启动时通过 Ort::ThreadingOptions 创建全局 ONNX 环境,并允许通过 YAML 参数 intra_threads 控制算子内并行线程数,默认设为 1 以避免与实时调度产生竞争。
sequenceDiagram
participant Inf as inference线程
participant ONNX as ONNX Runtime
participant Robot as RobotInterface
loop 每 dt*decimation
Inf->>Inf: update_obs_segments / flatten
Inf->>Inf: update_stacked_obs (帧堆叠)
Inf->>ONNX: session->Run(...)
ONNX-->>Inf: output_buffer
Inf->>Inf: clamp + action_scale + default_angle
Inf->>Robot: publish_action (写入act_)
end
若单帧推理耗时超过周期,系统会打印 Inference loop overran! 警告,帮助开发者识别模型复杂度或线程竞争问题。
Sources: inference_node.hpp, inference_node.cpp
多策略模型支持
本项目支持在内存中同时驻留多个策略模型,通过 active_policy_idx_ 切换当前活跃策略。每个策略在 load_config() 阶段被解析为独立的 PolicyRuntime,拥有自己的 ModelContext、obs_layout 与 frame_stack。这意味着不同策略可以加载完全不同的 ONNX 文件、观测维度与帧堆叠深度,例如:
- 基础行走策略:
policy.onnx,frame_stack=10,需要cmd_vel输入; - 动作模仿策略:
policy_wave.onnx,frame_stack=1,需要motion_pos与motion_vel输入。
切换策略时,推理循环仅改变 active_policy() 的引用目标,不需要重新加载模型或重建会话。关于策略切换的触发机制与状态迁移,请参阅 多策略运行时与热切换机制。
Sources: ros_interface.cpp, inference_node.hpp
性能优化配置
ONNX Runtime 的 SessionOptions 在 setup_model() 中进行了四项针对性优化:
| 优化项 | 配置值 | 作用 |
|---|---|---|
DisablePerSessionThreads |
— | 复用全局线程池,降低线程创建开销 |
EnableCpuMemArena |
— | 启用 CPU 内存池,减少页分配 |
EnableMemPattern |
— | 启用静态内存规划,提升缓存局部性 |
SetGraphOptimizationLevel |
ORT_ENABLE_ALL |
开启全部图优化(常量折叠、算子融合等) |
这些选项的组合确保了在 CPU -only 场景下获得最低的推理延迟与最稳定的内存占用。配合 Ort::Env 级别的 ThreadingOptions 将 intra-op 线程限制在 1 条,有效避免了多线程调度对 SCHED_FIFO 控制线程的干扰。
Sources: inference_node.cpp
观测帧堆叠与张量形状
对于时序策略模型,单次观测往往不足以表达运动状态。系统通过 update_stacked_obs() 支持两种帧堆叠顺序,将多帧历史观测拼接到 input_buffer 中:
| 堆叠顺序 | 说明 | 适用场景 |
|---|---|---|
frame_major |
先按帧排列,每帧内部按观测字段排列 | 常规时序策略 |
obs_major |
先按观测字段排列,每个字段内部按帧排列 | 需要字段级时序对齐的模型 |
堆叠后的总输入尺寸为 obs_num * frame_stack + extra_obs_num,其中 extra_obs 不参与堆叠(常用于外部感知特征)。setup_model() 在初始化时严格校验该尺寸与 ONNX 模型输入张量是否一致,防止运行时形状错误。
Sources: inference_node.cpp, inference_node.cpp
约束与故障排查
在使用 ONNX Runtime 推理引擎时,需要特别注意以下约束:
- 单输入限制:当前实现仅支持单输入 ONNX 模型。若导出模型时误留多个输入,
setup_model()会抛出Only single-input ONNX models are supported异常。 - 尺寸一致性:YAML 中的
obs_layout、frame_stack与模型实际输入维度必须严格匹配,否则在节点启动阶段即报错终止。 - 动态 batch:代码将输入 shape 的第一维
-1自动替换为1,仅支持 batch=1 的推理。 - 实时超限:若频繁出现
Inference loop overran!,可尝试增大decimation、减小frame_stack,或在更高性能平台上运行。
与其他模块的衔接
ONNX Runtime 推理引擎并非独立工作,其上下游关系如下:
- 上游:观测值由 观测值组装、归一化与帧堆叠 负责从 IMU、电机状态、手柄指令等来源聚合,并经
update_stacked_obs()写入输入缓冲区。 - 下游:推理输出的原始动作经
action_scale_缩放与默认角度偏移后,通过RobotInterface::apply_action()下发至 电机驱动与 CAN/CANFD 通信 模块。
如果你需要新增一种观测源并喂入 ONNX 模型,除了修改 YAML 配置外,还需在 观测布局配置与动态解析 中注册对应的 ObsSourceDefinition。