PreTrainedConfig.from_pretrained() leaves pretrained_path=None → silently builds untrained processor pipeline (blind policy)
Description
PreTrainedConfig.from_pretrained(path) returns a parsed config whose pretrained_path is None, because the serialized config.json only carries train-time fields and the path is never backfilled (source). The CLI path is unaffected — the draccus parser injects --policy.path into the config — so this only bites the documented programmatic path.
Why it's nasty
If you build a policy manually (the natural way for notebooks / bridges / serving code):
cfg = PreTrainedConfig.from_pretrained("lerobot/diffusion_pusht")
policy = make_policy(cfg=cfg, env_cfg=PushtEnv()); policy.eval()
pre, post = make_pre_post_processors(policy_cfg=cfg, pretrained_path=cfg.pretrained_path)then cfg.pretrained_path is None, and:
make_policy()logs a singlelogging.warningabout "instantiating a policy from scratch" (easy to miss, looks informational),make_pre_post_processors(pretrained_path=None)skips loading the safetensors and builds a fresh processor pipeline with empty normalization stats — no error, no exception,- the resulting
NormalizerProcessorStepis effectively an identity op, so raw pixel values (0-255/255) and raw agent positions (0-512 px) go straight into the network.
The policy runs, produces actions, steps the env — and scores ~0. On the migrated lerobot/diffusion_pusht checkpoint + PushT: same rollout code scored cum 4.8 / max 0.016 via this path vs cum 118-199 / max 0.98-1.0 once the path is backfilled (or via the CLI, which injects it).
Minimal reproduction
- Migrate an old checkpoint:
python -m lerobot.processor.migrate_policy_normalization --pretrained-path lerobot/diffusion_pusht --output-dir /tmp/m(then copyconfig.json+model.safetensorsfrom the HF snapshot — note the migration script currently emits an empty config.json and does not copy weights, see #4370-area discussion; happy to file that separately). - Run the snippet above against
/tmp/mwith a PushT rollout loop — observe reward ≈ 0 and a single from-scratch warning. - Add
cfg.pretrained_path = "/tmp/m"afterfrom_pretrained— same code now converges (max reward ~1.0).
Diagnosis was only possible by dumping the preprocessor pipeline: the normalizer's state_dict() is empty on the broken path and fully populated on the working one.
Proposed fix
Backfill pretrained_path / pretrained_revision in from_pretrained() before returning (mirrors what the CLI parser does). PR incoming with a regression test.
Env: lerobot 0.6.1, torch 2.11.0+cu130, Ubuntu 24.04 (WSL2).
Source: huggingface/lerobot