🤖 roboto_origin_03 Wiki
首页 / 部署 / 观测堆叠与多策略切换

推理节点并非简单地将当前传感器数据喂给单一 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_stackstack_order 两个参数实现可配置的观测滑动窗口。每个策略在 PolicyRuntime 中独立保存堆叠参数,obs 向量保存当前帧的扁平化观测(长度 obs_num),而 ctx->input_buffer 保存最终送入 ONNX 的时序张量(长度 frame_stack * obs_num + extra_obs_num)。

堆叠顺序

ObsStackOrder 枚举定义两种排布方式,直接决定 ONNX 输入张量的内存布局:

滑动窗口算法

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 是一份完整的策略运行时沙箱,包含:

这种设计使得多个 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.onnxpolicy_dance.onnxpolicy_punch.onnx(各绑定 .npz 动作序列)。运动策略的观测布局通常以 motion_posmotion_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() 线程的每一周期,激活策略的观测处理流水线如下:

  1. 调用 update_obs_segments(),根据 obs_layout 依次执行成员函数指针,填充各分段;
  2. flatten_obs_segments() 将分段拼接为 policy.obs
  3. policy.obsclip_observations_ 截断;
  4. update_stacked_obs() 将截断后的观测滑入 ctx->input_buffer 的堆叠区;
  5. 若存在 extra_obs_layout,同理更新并拼接到堆叠区之后;
  6. 若策略绑定运动文件,推进 motion_frame
  7. 标记 is_first_frame = false
  8. 执行 ONNX Run
  9. 输出经 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 动作参考与最终动作发布。若需接入自定义强化学习策略,可进一步阅读 接入自定义强化学习策略 了解如何匹配本系统的观测布局协议。