Vision encoder is lerobot's default: ResNet-18 with ImageNet weights and BatchNorm — a deliberate departure from Chi et al. §3.2, which specifies ResNet-18 without pretraining and BatchNorm replaced by GroupNorm. The paired run is dp_so101_cubcyl_recovery_tp48_paper_3cam, which follows the paper.
This arm LOST the A/B. The paper-recipe encoder beat it at every single eval point — see the table below. That was the opposite of the pre-registered prediction, so on this dataset spatial-softmax + GroupNorm generalises better than an ImageNet prior.
🚨 THIS IS THE 20,000-STEP CHECKPOINT, NOT THE FINAL ONE
The run went to 100k steps. Held-out loss got monotonically worse the entire time.
Train loss ended at 0.002 while held-out loss rose 4× — a ~32× gap, over 98 epochs of 113 training episodes.
The true optimum is 10k and it was never saved (save_freq=20000). This is the best checkpoint that exists, not the best checkpoint possible. A re-run with dense checkpointing is the obvious fix and is not done yet.
Leading explanation: the paper's recipe assumes an EMA of the weights, and lerobot implements no EMA — no EMAModel, no use_ema anywhere in the package. EMA is a strong implicit regulariser, so this is plausibly "we removed the paper's regulariser and then trained 98 epochs", not "the dataset is too small". Untested.
One extra caution specific to this arm. BatchNorm's running statistics are frozen at deployment and encode the training-time image distribution, including lighting and exposure. Our webcams cannot lock exposure (OpenCVCameraConfig in lerobot 0.6.0 has no exposure/gain/white-balance field), so lighting genuinely drifts between sessions. GroupNorm re-normalises each frame independently and adapts; BatchNorm does not. Held-out eval_loss cannot detect this — it is computed on the same recorded frames — so this arm may underperform on the arm by more than the loss gap suggests.
⚠️ eval_loss here is noise-prediction MSE, not a task metric. It averages over uniformly sampled diffusion timesteps, where the easy high-noise regime dominates and the hard low-noise regime is what actually sets action precision. A 4× degradation is a strong negative signal, not proof the policy is unusable. Nobody has rolled this out yet.
🚨 Three things will silently break this policy
1. Inference latency — Diffusion Policy has a hard real-time deadline
Unlike ACT, which runs its network once per chunk, DP runs the UNet num_inference_steps times. Measured on a MacBook Air (MPS, fp32, batch 1):
component
cost
3 × ResNet-18 encoders, 2 frames each, 216×288
74.6 ms
UNet × DDIM-16 at Tp=48
219.8 ms
total per chunk
294.4 ms
The deadline is n_action_steps / fps. At 30 fps:
n_action_steps
budget
verdict
8
267 ms
❌ misses
24 (shipped)
800 ms
✅ 506 ms headroom
⇒ Do not lower n_action_steps below ~10 on this hardware. And do not raise num_inference_steps toward lerobot's default of 100: that alone costs ~1370 ms per chunk and the policy simply will not keep up. Missing the deadline raises no error — lerobot-rollout logs Record loop is running slower than the target FPS and the arm stalls or stutters.
n_action_steps is safely tunable at rollout across 1…48 (it does not enter the loss), so sweep it — just stay above ~10.
2. Calibration frame
A policy emits joint angles in the calibration frame of the machine that recorded its training data. Run it against a different one and the arm moves smoothly to the wrong place, with no error.
Confirmed on the arm 2026-08-10. And separately: where the arm's base is clamped is part of that frame too — a re-clamp shifted ours right and every policy reached right of the object, while every config file stayed byte-identical. See so-arm101 setup §3b-bis and §3c.
3. Camera keys and their physical mapping
Needs exactly three: observation.images.wrist, observation.images.front, observation.images.top, all 3 × 480 × 640 (resized to 240×320 and centre-cropped to 216×288 internally). Miss one and it will not load; swap two and it loads fine and behaves badly.
⚠️ Verify index → physical camera every session. macOS has no udev and indices reorder silently.
What it is
Policy
Diffusion Policy (CNN/UNet, FiLM conditioning)
Parameters
102,285,724 tensors total · 68,665,222 UNet (extra tensors are BatchNorm running stats)
resize 240×320 → random crop 216×288 (centre crop at eval)
Steps · batch · seed
20,000 (of a 100k run) · 64 · 1000
lr / wd / schedule
1e-4 / 1e-6 / cosine, 500 warmup
Parameters follow Table 7's real-world rows of the paper, with one deliberate departure: Tp/Ta = 48/24 rather than the paper's 16/8. The paper ran at 10 Hz, where Ta=8 is 800 ms of open loop. At 30 fps the same duration needs Ta=24. Copying the step counts would have produced a policy that misses its deadline every chunk (see §1).
Training data
phi_so101_cubes_cylinder_recovery_v1 — 143 episodes / 81,943 frames = 120 clean + 23 failure-then-recovery. All 23 recovery episodes in training; the 30-episode holdout is clean-only:
Identical to the split used by the ACT models, so the two are comparable on the same held-out episodes.
Eval
Not scored on the arm. No success rate exists.
The baseline to beat, from the ACT work — act_so101_cubcyl_poshold_chunk50_cvae_3cam, 11 held-out rollouts, 2026-08-10: 27% success / 0.418 mean progress. Per object: red cube 67%, yellow cylinder 25%, white cube (45 mm) 0% (0/4).
⚠️ DP's eval_loss cannot be compared to ACT's. DP's is noise-MSE; ACT's is L1 + KL on actions. Different quantities, different scales. DP vs ACT is a rollout question only.
Three confounds to report alongside any DP-vs-ACT number:
this model
ACT
input resolution
216×288 → 7×9 ResNet grid
480×640 → 15×20
open-loop commit
800 ms
1670 ms
encoder init
ImageNet
ImageNet
Method vs method as published, not a controlled comparison of action decoders.
Provenance
Script configs/hpc/train_diffusion_recovery.sbatch, write-up experiments/2026-08-12_dp-recovery-encoder-ab.md, W&B project phi_dp, SLURM array 9088486 task 1, 7 h 11 m on one H200.