Fine-tune: 121 plate-pick episodes collected on the customer's robot (ppv4 + ppv5 setup — updated visual alignment and arm position)
Architecture: PaliGemma 2B VLM backbone + SigLIP ViT-So400m vision encoder + FAST DCT/BPE action tokenizer; ~50 M trainable LoRA parameters
Action: 7-DoF — [j1..j6 radians, gripper in [0,1]] for the right arm only
Chunk size: 10 steps at 20 Hz = 500 ms of motion per inference call
Why we retrained (ft-v2 vs ft-v1)
ft-v1 (ckpt-500, 08062026_single_plate_v10, 52 episodes) failed in deployment because the customer's robot setup changed between data collection and deployment:
What changed
Impact
Camera mount / visual alignment
SigLIP embeddings shifted — model "sees" a different scene
Robot arm home position
State distribution mismatch — model starts in unknown territory
ft-v2 retrains from main/4000 on 121 episodes from the new setup (PranayTest/piper-ppv4-ppv5), fully covering the updated visual and kinematic environment.
Early stop rule: val rose at the next eval after best → stopped at 2500, deployed step 2000.
Dataset details (ppv4ppv5)
Raw format: per-episode directories with front.mp4, right.mp4, meta.json, telemetry.jsonl.
Converted to LeRobot v2.1 by scripts/ppv4ppv5_to_lerobot.py.
Field
Value
Source
PranayTest/piper-ppv4-ppv5 (HF, private)
Episodes used
006–126 (000–005 skipped: test runs / no plate)
Joint units in raw
Degrees — converted x pi/180
Telemetry rate
~17.83 Hz — resampled to 20 Hz via linear interpolation
All in radians. Gripper 0.985 = open (1.0 = fully open, 0.0 = closed).
Starting significantly outside this range degrades performance — all 121 training episodes began here.
Inference
The model is served as a FastAPI server. See scripts/serve_api.py.
python
1import requests, base64, io, numpy as np
2from PIL import Image
34URL ="http://<host>:<port>/predict?token=<VAST_TOKEN>"5API_TOKEN ="<API_TOKEN>"67defencode(arr):8 buf = io.BytesIO()9 Image.fromarray(arr).save(buf,format="PNG")10return base64.b64encode(buf.getvalue()).decode()1112r = requests.post(URL,13 json={14"front_image_b64": encode(front_rgb),# any resolution HxWx3 uint8 RGB15"right_image_b64": encode(right_rgb),# server resizes to 640x48016"state":list(map(float, state_7)),# current joints + gripper17"prompt":"pick up the plate"18},19 headers={"Authorization":f"Bearer {API_TOKEN}"},20 timeout=30)21actions = np.array(r.json()["actions"])# shape (10, 7) float32
Execution: apply each row at 20 Hz (50 ms per step).
Pipelining: keep 4–8 requests in flight — inference is ~0.8 s but each chunk covers 0.5 s, so naive single-call loop gives < 15% duty cycle.
Repository structure
checkpoints/
ppv4ppv5_run1/
2000/ <- deployed (val loss 0.878)
params/ <- Orbax checkpoint (~8.2 GB bfloat16)
assets/ <- norm_stats.json
500/ <- val 1.009
1000/ <- val 0.911
1500/ <- val 0.904
2500/ <- val 0.891
scripts/
serve_api.py <- FastAPI inference server (logging, auth, image resize)
ppv4ppv5_to_lerobot.py <- dataset converter: raw ppv4ppv5 -> LeRobot v2.1
config.py <- openpi TrainConfig (piper_h_ppv4ppv5_ft)
data_loader.py <- openpi data loader (patched)
piper_policy.py <- PiperInputs / PiperOutputs transforms
train.py <- openpi training script (patched)
loss_curve.png <- training loss plot
Fine-tune from main/4000 — 52 customer episodes (08062026_single_plate_v10)
ft-v1 / ckpt-500
Phase 3
Deploy ft-v1 on Vast.ai
Live at old URL — model failed (env changed)
Phase 4
Retrain from main/4000 — 121 episodes from new setup (ppv4+ppv5, updated camera + arm position)
ft-v2 / step-2000 · val 0.878
ft-v1 failed because the robot setup changed (camera alignment + arm home pose) between collection and deployment. ft-v2 was collected and trained entirely on the updated setup.
Contact
Model trained by Kavin (kavinrajk.r.2005@gmail.com).
Dataset collected by the customer on AgileX Piper-H hardware.