PPS β PiperX bimanual, grind-and-tamp
Proxy Policy Steering for the PiperX bimanual robot on axiboai/piper_tamping
("grind coffee beans and then tamp").
Steering never modifies the base policy. At every denoising step the two proxies' velocity difference is added to the frozen base velocity:
v = v_base + steer_scale * (v_task - v_reference)
Contents
| dir | what | notes |
|---|---|---|
base/ |
frozen pi0.5, 14-D bimanual joint space | PyTorch conversion of axiboai/pi05_piperx_filter_joint @ 15000 |
reference/ |
reference proxy, distilled from base/ |
step 12000 |
task/ |
task proxy, fine-tuned from reference/ on tamping demos |
step 8000 |
All three carry byte-identical action norm stats (the base filter_joint set). That is required: the residual is only meaningful if the three velocity fields live in one normalized space.
task/ was initialized from this reference/. They are a matched pair β
the residual cancels only because both proxies share an initialization and so
share their approximation error. Do not mix with a reference from another run.
Optimizer state is not included; these are for inference, not resuming.
Configuration
- base:
pi05_piperx_bimanual, action_horizon 50, padded action_dim 32 - proxies:
proxy_piperx_bimanual_pi05, action_dim 14, DINOv3 ViT-S/16 + gemma_12m - wrist-only:
cam_left_wrist,cam_right_wrist;base_0_rgbis zero-padded and masked off - state/action layout:
[L_arm_6 (rad), L_grip (m), R_arm_6 (rad), R_grip (m)]
Serving
Code: https://github.com/axiboai/piperx-pps
git clone https://github.com/axiboai/piperx-pps && cd piperx-pps/openpi
uv sync && scripts/install_transformers_replace.sh
python fetch_dinov3.py
hf download axiboai/pps_piperx_tamping --local-dir checkpoints/pps_piperx_tamping
CK=checkpoints/pps_piperx_tamping
uv run python scripts/steer_policy.py \
--base_model_name pi05_piperx_bimanual --base_checkpoint_dir $CK/base \
--steer_model_name proxy_piperx_bimanual_pi05 --steer_checkpoint_dir $CK/task \
--mimic_model_name proxy_piperx_bimanual_pi05 --mimic_checkpoint_dir $CK/reference \
--steer_scale 0.4 --num_steps 10 --port 8000
--steer_scale 0.4 is the paper default; 0.4β0.6 is the useful band. 0 recovers
the unsteered base, and --only_steer discards the base velocity entirely.
Inference skips the repack transform, so a client sends observations already in the transform key layout:
{
"state": np.ndarray, # (14,) radians + metres
"images": { # CHW uint8, or float in [0,1]
"cam_left_wrist": ...,
"cam_right_wrist": ...,
},
"prompt": "grind coffee beans and then tamp",
}
Training
Both proxies trained on a single RTX 5090 (32 GB).
- reference:
distill_pytorch.py, batch 8, peak LR 6.25e-6, 12000 steps - task:
train_pytorch.py, batch 8, peak LR 1.5e-6, fromreference/12000
Batch size and LR move together. At batch 8 the inherited 2.5e-5 (tuned for the DROID proxies at batch 32) diverges: distillation loss bottoms near 0.08 by step 3000, then climbs past 0.9. The task proxy needed a further reduction to 1.5e-6 β at 6.25e-6 it bottomed at 0.170 by step 1500 and rose to 0.31 by step 10000.
Task-proxy loss, mean over 1000-step windows:
| steps | loss |
|---|---|
| 0β999 | 0.2050 |
| 1000β1999 | 0.1580 |
| 2000β2999 | 0.1528 |
| 3000β3999 | 0.1523 |
| 4000β4999 | 0.1516 |
| 5000β5999 | 0.1547 |
| 6000β6999 | 0.1500 |
| 7000β7999 | 0.1486 |
| 8000β8999 | 0.1498 |
Flat from ~3000 on; task/8000 closes the best window. Training was stopped at
step 9203 rather than the scheduled 20001, since the plateau was unambiguous.
Not yet validated on hardware. The loss curves say the proxies converged; they do not say the steered policy completes the task. The base was fine-tuned on a different task (portafilter insertion) and has never seen tamping, which is the intended PPS regime, but the success rate is unmeasured.