Cross-Backend Config Guard¶
When replaying across backends (train on one backend, eval on another), UniLab automatically checks whether the target backend config is compatible with the policy contract captured at training time, so a mismatched config can’t silently load a broken policy. This happens with no manual steps.
An example that replays successfully¶
For go2_joystick_flat, the MuJoCo and Motrix owners agree on every guarded field, so a cross-backend replay passes directly:
# 1) Train in MuJoCo, producing a checkpoint
uv run train --algo ppo --task go2_joystick_flat --sim mujoco
# 2) Replay the same checkpoint across backends in Motrix — the guard passes, playback runs
uv run eval --algo ppo --task go2_joystick_flat --sim mujoco --load-run -1
How the guard chain works¶
At train time:
ExperimentTrackersnapshots the contract fields that define policy I/O intocontract_snapshotinrun_config.json(the checkpoint format is untouched, so historical checkpoints stay compatible).At replay time:
evalloads the target backend owner config selected by--sim(e.g.src/unilab/conf/ppo/task/go2_joystick_flat/motrix.yaml) and injectstraining.play_only=true. If the task has no owner config for the requested backend,evalfalls back to a sibling backend owner of the same task and re-applies the requested backend through the allowlistedtraining.sim_backendoverride (trainstill requires the owner config to exist); the guard chain below is unaffected.Before env creation: the play entrypoints (rsl_rl / appo / sac / flashsac) call
resolve_sim2sim_config, comparing the target config against the source run’s contract snapshot field by field.At weight load:
policy_load_dim_guardwraps checkpoint loading, re-raising cryptic tensor shape-mismatch errors as a clear sim2sim diagnostic.
What the guard covers¶
Fields are classified by dotted path into three tiers (see src/unilab/utils/sim2sim.py):
Tier |
Behavior |
Fields |
|---|---|---|
DENYLIST |
Mismatch → |
|
WARNING_LIST |
Prints a warning, continues |
|
ALLOWLIST |
Free to override, not checked |
|
When DENYLIST fields differ¶
If the target backend’s DENYLIST fields differ from training (e.g. a task whose two owners use different action_scale values), the guard aborts before env creation and lists the diverging fields. Two ways to resolve:
Align the contract (recommended): make the target owner’s DENYLIST fields match the training backend, then replay.
Force through (at your own risk):
uv run eval ... training.sim2sim_strict=falsedowngrades DENYLIST mismatches to warnings. It does not bypass the snapshot-integrity checks below.
Minimum supported snapshot format: the guard fails closed (raises
CrossBackendIncompatibleError) when the source run’srun_config.jsonis missing, is not valid JSON, has nocontract_snapshotmapping, or contains keys outside the current contract schema — including the retired legacy keysenv.sampling_mode,algo.policy.actor_hidden_dims,algo.policy.critic_hidden_dims, andalgo.empirical_normalization, which are no longer aliased. Starting from the first release that ships this change, only canonicalcontract_snapshotmappings written by training runs on the 1.3.5 baseline schema are supported, and the breaking change is not backported to 1.3.5 patch releases. There is no offline migration tool: to reuse an older run, retrain the policy with the current version (checkpoint weights are unchanged) and replay from the new run.
Manager-Based snapshots store the complete typed observation and action declarations from
Hydra. A snapshot from before those fields existed cannot prove that its policy I/O is
equivalent to a Manager-Based target, so asymmetric presence fails closed. Set
training.sim2sim_strict=false only as an explicit user override; the load-time dimension
guard still remains active.