推理节点并非简单地将当前传感器数据喂给单一 ONNX 模型,而是维护了一套可配置的观测编排系统:支持多路异构观测源的动态组合、时序堆叠,以及在同一进程内托管多个策略并在它们之间安全切换。本文档从观测源注册、堆叠算法、双轨输入缓冲、多策略运行时到切换控制,逐层拆解其设计原理与实现细节。
观测源注册表与布局解析
系统内部维护一份编译期固定的观测源注册表,每个源通过成员函数指针与名称绑定。当前共定义十种观测源,涵盖本体感知、运动参考、外部感知与中断信号四类。
| 观测源 | 维度 | 数据来源 | 典型用途 |
|---|---|---|---|
ang_vel |
3 | IMU 角速度 | 本体姿态变化率 |
gravity_b |
3 | IMU 四元数转机体重力向量 | 姿态估计与跌倒检测 |
cmd_vel |
3 | 手柄或 /cmd_vel |
用户运动指令 |
dof_pos |
23 | 电机反馈经 usd2urdf 映射 |
关节位置(相对默认角度) |
dof_vel |
23 | 电机反馈经 usd2urdf 映射 |
关节速度 |
last_action |
23 | 上一帧策略输出 | 动作平滑与历史依赖 |
motion_pos |
23 | .npz 运动文件当前帧 |
模仿学习参考位姿 |
motion_vel |
23 | .npz 运动文件当前帧 |
模仿学习参考速度 |
interrupt |
1 | 布尔标志位 | 触发关节中断/接管 |
perception |
可配 | /elevation_data 等外部话题 |
地形感知等高层特征 |
这些源在 obs_source_definitions() 中以 ObsSourceDefinition 数组静态注册,名称与成员函数指针一一对应。配置加载阶段,parse_obs_layout() 将形如 "ang_vel:3, gravity_b:3, cmd_vel:3, dof_pos:23, dof_vel:23, last_action:23" 的字符串解析为 ObsSourceSpec 序列,校验名称存在性与维度合法性。解析后的布局决定了后续观测分段(obs segment)的生成顺序与尺寸。
Sources: obs_manager.cpp
观测堆叠机制
强化学习策略通常需要短期时序上下文,而非单帧瞬时状态。系统通过 frame_stack 与 stack_order 两个参数实现可配置的观测滑动窗口。每个策略在 PolicyRuntime 中独立保存堆叠参数,obs 向量保存当前帧的扁平化观测(长度 obs_num),而 ctx->input_buffer 保存最终送入 ONNX 的时序张量(长度 frame_stack * obs_num + extra_obs_num)。
堆叠顺序
ObsStackOrder 枚举定义两种排布方式,直接决定 ONNX 输入张量的内存布局:
- FrameMajor(帧优先):先放第 0 帧全部观测,再放第 1 帧全部观测,依此类推。适用于大多数卷积或 MLP 策略。
- ObsMajor(观测域优先):先将观测域 A 的
frame_stack个历史值连续存放,再将观测域 B 的frame_stack个历史值连续存放,依此类推。适用于需要对单个物理量做时序卷积或注意力的网络结构。
滑动窗口算法
update_stacked_obs() 实现了原位滑窗更新。对于 FrameMajor,逻辑为:将已有缓冲区整体前移 obs_num 个 float,在尾部填入最新一帧。对于 ObsMajor,则按每个观测域的 field_size 分别前移并补新值。首帧(is_first_frame == true)特殊处理:用当前观测填满整个历史窗口,避免冷启动时的零值冲击。
Sources: inference_node.cpp
双轨观测输入:堆叠观测与附加观测
除了参与时序堆叠的 obs_layout 外,系统还引入了 extra_obs_layout 机制。附加观测不参与滑窗,每帧直接拼接到堆叠缓冲区末尾。这种“双轨”设计让高频时序特征(如关节状态)与低频大维度特征(如 187 维地形感知)可以共存于同一输入张量,而不必为后者浪费 frame_stack 倍的冗余存储。
以 inference_attn_enc.yaml 为例:基础观测布局 obs_layout 包含角速度、重力向量、指令、关节状态等共 78 维,设置 frame_stack: 5;而 extra_obs_layouts 中的 perception:187 作为附加观测,直接以当前帧的 187 维地形数据拼接到 78×5 的堆叠缓冲区之后,形成 ONNX 的最终输入。
Sources: inference_node.cpp, inference_attn_enc.yaml
多策略运行时架构
推理节点在初始化时根据 model_names 构建 std::vector<PolicyRuntime> policies_。每个 PolicyRuntime 是一份完整的策略运行时沙箱,包含:
- 观测侧:
obs_layout、extra_obs_layout、obs_segments、extra_obs_segments、obs(当前帧扁平值) - 时序侧:
frame_stack、stack_order、is_first_frame - 运动侧:可选的
MotionLoader与motion_frame计数器 - 推理侧:独立的
ModelContext,内含 ONNXOrt::Session、input_tensor、output_tensor与缓冲区
这种设计使得多个 ONNX 模型可以在同一进程内并存,各自拥有独立的输入维度、堆叠深度与观测组合,而节点通过 active_policy_idx_ 原子地指向当前激活策略。构造阶段遍历所有策略,逐一调用 setup_model() 创建 ONNX 会话,并校验模型输入尺寸与配置计算出的 obs_num * frame_stack + extra_obs_num 严格一致。
Sources: inference_node.hpp, inference_node.cpp
策略切换模式
系统支持两种截然不同的多策略切换语义,由配置中的观测源与运动文件共同决定。
中断模式(Interrupt Mode)
当任意策略的观测布局中包含 interrupt:1 时,节点启用中断支持。此时通常只配置单一策略(如 policy_interrupt.onnx),interrupt 观测源在 get_interrupt_obs() 中读取 is_interrupt_ 原子布尔。手柄 LB 键按下时,通过 switch_while_paused 安全切换中断标志。在推理循环中,若中断激活,策略输出的后 10 个关节角度会被 /joint_ref_states 话题传入的外部动作覆盖,实现“策略主干 + 外部接管局部关节”的协作控制。
Sources: ros_interface.cpp, obs_manager.cpp, inference_node.cpp
运动策略模式(Motion Policy Mode)
当配置中存在带 motion_names 的策略时,节点启用运动策略支持。典型配置如 inference_beyondmimic.yaml,包含一个基策略 policy.onnx(无运动文件)与三个运动策略 policy_wave.onnx、policy_dance.onnx、policy_punch.onnx(各绑定 .npz 动作序列)。运动策略的观测布局通常以 motion_pos 和 motion_vel 替换 cmd_vel,使策略跟随预录动作参考。
此模式下 LB 键在“基策略 ↔ 当前选中的运动策略”之间切换;RB 键则在后台循环选择下一个运动策略(仅在处于基策略时允许切换,避免运动播放中途跳转)。切换同样通过 switch_while_paused 完成:暂停推理、切换索引、重置目标策略的运行时状态(清空观测缓冲区、重置运动帧计数器)、再恢复推理。
Sources: ros_interface.cpp, inference_beyondmimic.yaml
切换安全机制
switch_while_paused 是一个通用 lambda 包装器,执行任何模式变更前先将 is_running_ 置为 false,变更完成后再视情况恢复。这保证了策略切换期间不会有半完成的推理循环读写观测缓冲区。此外,mode_mutex_ 保护 active_policy_idx_ 与中断标志的读写,而 lb_switch_mutex_ 防止手柄连击导致切换重入。
Sources: ros_interface.cpp
运行时状态管理
每个策略的运行时状态可通过 reset_policy_runtime() 独立清零:观测分段、附加分段、ONNX 输入输出缓冲区全部置零,motion_frame 回零,is_first_frame 置 true。全局 reset_runtime_state() 则在节点级暂停推理、重置控制指令与感知缓存、并将所有策略的运行时状态一并清零。该函数在以下场景被调用:推理暂停(B 键)、电机初始化/反初始化(X 键)、关节重置(A 键)以及程序启动后的首次初始化。
Sources: inference_node.cpp, inference_node.cpp
配置映射与典型模式
下表汇总了仓库中五种典型配置在观测堆叠与策略切换维度的差异:
| 配置文件 | 策略数 | frame_stack | 附加观测 | 切换模式 | 核心用途 |
|---|---|---|---|---|---|
inference.yaml |
1 | 10 | 无 | 无 | 标准行走,时序堆叠 |
inference_amp.yaml |
1 | 1 | 无 | 无 | AMP 风格单帧策略 |
inference_attn_enc.yaml |
1 | 5 | perception:187 |
无 | 注意力编码器 + 地形感知 |
inference_interrupt.yaml |
1 | 10 | 无 | 中断模式 | 外部关节中断接管 |
inference_getup.yaml |
2 | 10 / 1 | 无 | 运动策略 | 行走 + 起身恢复 |
inference_beyondmimic.yaml |
4 | 10 / 1×3 | 无 | 运动策略 | 行走 + 多动作模仿 |
Sources: config/
推理循环中的观测流水线
在 inference() 线程的每一周期,激活策略的观测处理流水线如下:
- 调用
update_obs_segments(),根据obs_layout依次执行成员函数指针,填充各分段; flatten_obs_segments()将分段拼接为policy.obs;- 对
policy.obs做clip_observations_截断; update_stacked_obs()将截断后的观测滑入ctx->input_buffer的堆叠区;- 若存在
extra_obs_layout,同理更新并拼接到堆叠区之后; - 若策略绑定运动文件,推进
motion_frame; - 标记
is_first_frame = false; - 执行 ONNX
Run; - 输出经
clip_actions_截断、按action_scale_缩放并叠加joint_default_angle_后写入电机目标。
flowchart TD
A[update_obs_segments<br/>按 obs_layout 填充分段] --> B[flatten_obs_segments<br/>拼接为 policy.obs]
B --> C[clip observations]
C --> D[update_stacked_obs<br/>滑入时序窗口]
D --> E[update extra_obs_segments<br/>拼接附加观测]
E --> F[step_motion_frame<br/>推进运动参考帧]
F --> G[ONNX Session Run]
G --> H[clip & scale actions<br/>叠加默认角度]
H --> I[写入电机目标 /action]
Sources: inference_node.cpp
与前后环节的衔接
观测堆叠与多策略切换处于推理节点的核心调度层。其上游是 RobotInterface 硬件抽象层 提供的 IMU 与关节数据,以及 ONNX 模型加载与实时推理 中构建的 ONNX Runtime 会话;下游则是 动作序列加载与运动策略 中的 MotionLoader 动作参考与最终动作发布。若需接入自定义强化学习策略,可进一步阅读 接入自定义强化学习策略 了解如何匹配本系统的观测布局协议。