#4647·lerobot

PreTrainedConfig.from_pretrained() leaves pretrained_path=None → silently builds untrained processor pipeline (blind policy)

Author: DanMo661Created Sep 15, 2026Updated Sep 15, 2026
Labelsbugpoliciestestsconfigurationprocessorexamplestraining

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):

python
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:

  1. make_policy() logs a single logging.warning about "instantiating a policy from scratch" (easy to miss, looks informational),
  2. 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,
  3. the resulting NormalizerProcessorStep is 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

  1. Migrate an old checkpoint: python -m lerobot.processor.migrate_policy_normalization --pretrained-path lerobot/diffusion_pusht --output-dir /tmp/m (then copy config.json + model.safetensors from 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).
  2. Run the snippet above against /tmp/m with a PushT rollout loop — observe reward ≈ 0 and a single from-scratch warning.
  3. Add cfg.pretrained_path = "/tmp/m" after from_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).