ODE Policy — R1 Lite "insert flower"
Checkpoints of the ODE policy trained on real-robot demonstrations of the Galaxea R1 Lite right arm inserting a flower. The policy learns a velocity field ȧ = f(c, s, a) over the arm's joint targets and produces each 8-step action chunk by integrating that field (RK4) from the current target, instead of denoising a chunk from noise.
Files
| Path | Content |
|---|---|
checkpoints/{50,100,150,200,250,300}.ckpt |
Full training checkpoints (485 MB each): model, ema_model, optimizer state dicts + the pickled Hydra config. Serve the EMA weights (the loader does this by default). |
config/config.yaml, config/overrides.yaml |
Resolved training config and the command-line overrides of this run |
logs/logs.json.txt |
Per-step / per-epoch training log (loss, lr, epoch time, val_loss every 5 epochs) |
SHA256SUMS |
Checksums of every uploaded file |
Checkpoints are torch.save(..., pickle_module=dill) payloads written by the ODE_Policy training runner.
Loading them needs that codebase (roboverse_learn.il.policies.ode.ode_image_policy.ODEImagePolicy) and
torch.load(..., pickle_module=dill, weights_only=False) — only load checkpoints you trust.
Data
at237299966/r1lite_insert_flower_single:
LeRobot v2.1, 95 episodes / 23,034 frames at 15 Hz, single task "insert flower".
| Input / output | Definition |
|---|---|
| Cameras | head_rgb (720×1280) and right_wrist_rgb (360×640), padded to square then resized to 256×256 (same geometry as the robot client) |
agent_pos |
right-arm joint positions (6, rad) + right gripper opening (1, mm) |
ODE state a |
right-arm absolute joint targets (6, rad). Row convention: a[t] = commanded target at t−1 (episode start: measured joints) |
| Gripper head | binary, +1 close / −1 open; the label flips where the operator's gripper ramp starts |
| Window | 8 observation frames, 8-step action chunk |
Training
| Model | ResNet18 (GroupNorm, random crop) per camera, 8 frames → condition c (512); vector field = 4-layer FiLM-MLP, width 512; proprioception enters the field only (cond_use_s=false); separate gripper head |
| Integration | RK4, 8 control steps per chunk, vel_scale=0.05; observation fixed over the chunk (obs_update=fixed) |
| Start perturbation | 50% of samples, per-joint std [0.005, 0.0316, 0.0138, 0.005, 0.005, 0.005] rad, decay 0.7 per step |
| Optimisation | AdamW, lr 1.5e-4, cosine schedule over 300 epochs, warm-up 500 steps, EMA |
| Batch | 2 × A100-40GB (DDP), 48 per GPU = 96 global; 236 steps/epoch, 70,800 steps in total |
| Wall clock | 9 h 45 min (116 s/epoch) |
| epoch | 0 | 10 | 50 | 100 | 150 | 200 | 250 | 300 |
|---|---|---|---|---|---|---|---|---|
| train loss | 1.16 | 0.30 | 0.070 | 0.039 | 0.023 | 0.017 | 0.012 | 0.011 |
| val loss | 0.84 | 0.61 | 0.68 | 0.71 | 0.75 | 0.81 | 0.81 | 0.80 (ep 290) |
The validation loss is a weak signal: one held-out episode, raw (non-EMA) weights, with the training start perturbation still active. Its minimum is at epoch 15 (0.54) and it drifts up afterwards, so compare the early and late checkpoints on the robot before picking one.
Deployment
The robot side is the unmodified openpi-r1lite closed-loop client (main_r1lite_closedloop.py, 15 Hz,
executes row 0 of every (8, 14) reply). The server re-integrates a chunk every 8 ticks from the last
target it sent and walks the client through it, matching how the policy was trained:
python -m roboverse_learn.il.deploy.serve_policy \
--checkpoint checkpoints/100.ckpt --host 0.0.0.0 --port 8000 --device cuda:0 \
--gripper_close 1 --gripper_open 100 --gripper_ramp_ticks 27
python -m roboverse_learn.il.deploy.test_client --host 127.0.0.1 --port 8000 -n 40 --gripper_range 1 100 # pre-flight
Measured on an A100: ~19 ms on re-planning ticks, < 0.1 ms on the others; loopback round trip p95 29 ms
(budget 66.7 ms). Code: ODE_Policy repository, neural_ode/real/ (data + training) and
roboverse_learn/il/deploy/ (server).