Skip to content

[RFC] Cross-backend sim2sim config consistency: sidecar-based contract preservation #579

@TATP-233

Description

@TATP-233

Problem

在 UniLab 中,用户可以在 MuJoCo 或 Motrix 后端训练策略,然后在另一个后端进行 sim2sim(play)验证。然而,当前 play 脚本完全依赖当前 Hydra 合成的配置(即目标后端的 YAML)来创建环境,而同一个 task 的 mujoco.yamlmotrix.yaml 是独立维护的,经常出现不一致:

  • algo.obs_groups 结构不同(如 mujoco 用 actor,motrix 用 policy/critic)(?存疑,验证是否真的存在这个问题)
  • env.action_scale 不同(如 0.25 vs 0.5)
  • env.reward 权重、base_height_target、max_tilt_deg 不同
  • env.commandsdomain_randgait_phase_init_mode 等关键参数不同

当这些差异存在时,目标后端创建出的环境 obs/action 空间与训练时不匹配,导致策略网络输入维度错误或语义偏移,最终表现为崩溃或行为异常。由于 checkpoint(.pt仅保存权重,不携带训练时的配置,用户只能在运行时才能发现不一致,调试成本极高。

Root Cause

  1. Checkpoint 不自包含:rsl-rl/APPO/offpolicy/MLX PPO 的 .pt 仅保存 actor_state_dict 等权重,没有保存训练时的 env/algo 配置。
  2. Play 配置来源单一play_rsl_rl() 等入口使用当前 Hydra cfg 调用 BackendAdapter.build_play_env_cfg_override(),无法感知训练时的配置。
  3. YAML 无强制一致性:同一 task 的 backend-specific YAML 独立编辑,没有分层机制(如 base.yaml)来锁定跨后端必须一致的字段。

Proposed Solution(Sidecar + Diff-Merge)

Phase 1:最小侵入的 Sidecar 方案

利用训练时已存在的 run_config.json sidecar(由 ExperimentTracker.start() 写入,包含完整 resolved full_cfg),实现"训练配置恢复 + 目标后端覆盖"的合并机制。

新增模块 src/unilab/training/sim2sim.py

  • Sim2SimConfigResolver:显式定义三类字段列表
    • ALLOWLIST:允许 target 自由覆盖(sim_backend, scene, play_steps, domain_rand, noise_config 等运行时/后端字段)
    • WARNING_LIST:允许覆盖但打印 warning(simulate_action_latency, ctrl_dt 等物理参数差异)
    • DENYLIST:严格禁止覆盖,差异即报错(obs_groups, action_scale, reward 权重、commands.ranges/samplingpolicy.hidden_dimsempirical_normalization/obs_normalization 等影响策略兼容性的字段)
  • resolve_sim2sim_config(source_run_dir, target_cfg):读取 source run 的 run_config.json,按上述规则与 target cfg 合并,返回 resolved DictConfig

ExperimentTracker 自动保存 contract_snapshot

run_config.json 中新增一个自动生成的 contract_snapshot 字段:

def extract_contract_snapshot(full_cfg: DictConfig) -> dict:
    paths = DENYLIST | WARNING_LIST
    return {path: OmegaConf.select(full_cfg, path) for path in paths}

新增 DENYLIST 字段时 snapshot 自动包含,无需手动维护。

Play 入口改造

play_rsl_rl()play_appo()play_offpolicy()play_mlx_ppo()create_env() 之前插入:

cfg = resolve_sim2sim_config(load_path_dir, cfg) or cfg

后续逻辑完全不变,实现最小侵入。

运行时维度校验(最后一道防线)

create_env() 之后、模型加载之前,增加轻量校验:

assert env.obs_groups_spec == source_snapshot["obs_groups_spec"]
assert env.action_space.shape[0] == source_snapshot["action_dim"]

关键设计决策

  • 不修改任何 checkpoint (.pt) 格式:零算法侵入,天然兼容历史 checkpoint。
  • 默认允许覆盖,DENYLIST 显式保护:不在任何列表中的字段(如 wandb_project, save_interval)target 可自由覆盖,避免误报。
  • default_angles 从目标 backend keyframe 重新推导:不强制恢复 source 值,避免 asset 元数据泄漏(符合 Cold-path asset access 原则)。
  • commands 拆分保护commands.ranges/sampling(影响 obs 语义)在 DENYLIST;commands.limits(仅影响训练分布)在 ALLOWLIST。

Phase 2:YAML 分层(长期工程整洁度)

为高频 task(如 g1_walk_flat, go2_joystick_flat)引入 base.yaml

# conf/ppo/task/g1_walk_flat/mujoco.yaml
defaults:
  - base@_global_
  - _self_

sim_backend: mujoco
env:
  scene:
    model_file: ...

base.yaml 承载 TaskCore(跨后端必须一致),backend YAML 仅含 backend-specific override。配合 CI 检查确保 backend YAML 不能覆盖 DENYLIST 字段。

Phase 3:CI 回归测试

添加 cross-backend sim2sim 测试用例(如 MuJoCo 训练 10 iters → Motrix play 验证不崩溃)。

Deliverable

  • src/unilab/training/sim2sim.pySim2SimConfigResolver + resolve_sim2sim_config
  • ExperimentTracker 中自动 contract_snapshot 提取逻辑
  • 四个 play 入口的 resolve_sim2sim_config 集成(rsl-rl / APPO / offpolicy / MLX PPO)
  • 运行时维度校验(obs_groups_spec, action_dim)
  • 单元测试 tests/test_sim2sim_resolver.py
  • AGENTS.md 更新(sim2sim 规范与字段归属说明)

Definition of Done

  • MuJoCo 训练 g1_walk_flat → Motrix sim2sim 能正确加载并运行,obs/action 维度匹配。
  • 故意修改 target YAML 的 DENYLIST 字段(如 env.action_scale)时,Sim2SimConfigResolver 在 env 创建前抛出 CrossBackendIncompatibleError
  • 旧 run(无 contract_snapshot,即当前及历史 checkpoint)能 fallback 到当前 YAML 并打印 warning,不中断现有工作流。
  • make test-all 通过。
  • AGENTS.md 更新 sim2sim 相关规范。

Validation Plan

  1. 在 MuJoCo 后端训练 g1_walk_flat(少量 iteration),保存 checkpoint。
  2. 在 Motrix 后端 play 该 checkpoint,验证策略能正常推理且不报错。
  3. 手动修改 Motrix YAML 的 env.action_scale,再次 play,验证 resolver 在 env 创建前报错并提示差异字段。
  4. 运行 make test-all 确认无回归。

Dependencies and Blockers

无外部依赖,纯仓库内部 infra 改动。

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requesttype:infraInfrastructure or tooling work

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions