My first Results: PI0.5 VLA Policy

Hi!

Here are my first results for the AI for Industry Challenge . I’ve been working on an end-to-end VLA (Vision-Language-Action) policy to tackle the autonomous cable insertion task.

In this video, the robotic arm uses a PI0.5 architecture to interpret a natural language command and attempt the insertion of an SC/SFP connector in a Gazebo simulation, though, as you can clearly see, it doesn’t actually manage to plug any of them in yet! :joy: :rofl:

Technical Overview:

  • Architecture: PI0.5 (PaliGemma 2B + Gemma 300M Action Expert).
  • Software Stack: ROS 2 and the LeRobot framework.
  • Inference Hardware: NVIDIA RTX 5090.
  • Simulation: Gazebo Sim with RGB camera feeds and joint/wrench state feedback.

The policy already shows a strong semantic understanding of the task and good spatial reaching capabilities. I’m currently focused on fine-tuning the real-time control loop and the transition between the approach and the final insertion phase to compensate for inference latency.

Best regards!

5 Likes

same here tried with both standard lerobot ACT and Diffusion policy, gets roughly to near the port but has very poor accuracy.

1 Like

@bha51 I found the same.

I started gathering data for ACT/VLA policy training, but I’m hitting a pretty hard mental wall which is: if there are multiple SCs/NICs (which is possible at test-time), how can our policy know which one to choose? This feels like a strong blocker of any off the shelf VLA fine-tuning approach (i.e. you’d need a substantially larger number of episodes).

Curious if anyone has considered this. Happy to share some of my findings/thoughts.

1 Like

Hi @Andrew_Garrett

Great point! My approach is to label the dataset with specific text for each connector (sfp, sc) and slot (nic_card_mount_0, mount_1, …).

This forces the VLA to learn spatial grounding : mapping the text ID to a unique pixel region, even if the cards look identical.

However, to make this work, I think I need more data than what I have right now. I’m planning to scale up to 100 episodes per prompt (around 1,000 episodes total). Currently, I only have about 5 episodes per task (around 100 episodes), which is definitely not enough. Also, my initial tests were only 5,000 training steps , and I’m planning to increase that significantly.

Regards!

1 Like

Hello,

Its been a great learning experience by going over the threads in this forum. I am at a point where I have a system that is running end to end, with 160 episodes of training data trained on Pi0.5, SmolVLA and ACT. But, the runs don’t actually manage to plug any of SC/SFP connector in yet! Only ACT succeeded partially that too on trial 3.

[aic_engine-6] [INFO] [1777834496.506042364] [aic_engine]:

[aic_engine-6] [INFO] [1777834496.506046495] [aic_engine]: ║ ✓ All Trials Processed!

[aic_engine-6] [INFO] [1777834496.506049485] [aic_engine]: ║ Successful: 1 Failed: 2

[aic_engine-6] [INFO] [1777834496.506052085] [aic_engine]: ║ Total Score: 14.281 Complete Scoring Results ║

[aic_engine-6] [INFO] [1777834496.506445450] [aic_engine]: total: 14.280840038413849

[aic_engine-6] [INFO] [1777834496.506447780] [aic_engine]: trial_1:

[aic_engine-6] [INFO] [1777834496.506450360] [aic_engine]: tier_1:

[aic_engine-6] [INFO] [1777834496.506452050] [aic_engine]: score: 1

[aic_engine-6] [INFO] [1777834496.506454111] [aic_engine]: message: Model validation succeeded.

[aic_engine-6] [INFO] [1777834496.506456611] [aic_engine]: tier_2:

[aic_engine-6] [INFO] [1777834496.506458851] [aic_engine]: score: -36

[aic_engine-6] [INFO] [1777834496.506460641] [aic_engine]: message: Scoring succeeded.

[aic_engine-6] [INFO] [1777834496.506464851] [aic_engine]: contacts:

[aic_engine-6] [INFO] [1777834496.506466391] [aic_engine]: score: -24

[aic_engine-6] [INFO] [1777834496.506469092] [aic_engine]: message: Contacts detected (only first reported) between entity named [ur5e::gripper/hande_finger_link_l::gripper/hande_finger_link_l_fixed_joint_lump__finger_collider_box001_collision_1] and [task_board::nic_card_mount_4::nic_card_link::plane001_collider_box.006]. Penalty applied.

[aic_engine-6] [INFO] [1777834496.506527455] [aic_engine]: insertion force:

[aic_engine-6] [INFO] [1777834496.506529065] [aic_engine]: score: -12

[aic_engine-6] [INFO] [1777834496.506530945] [aic_engine]: message: “Insertion force above 20.00 N, detected for a time of 9.42 seconds. Max detected force: 72.43N. This is above the threshold of 1.00 seconds. Penalty applied.”

[aic_engine-6] [INFO] [1777834496.506547937] [aic_engine]: trial_2:

[aic_engine-6] [INFO] [1777834496.506549557] [aic_engine]: tier_1:

[aic_engine-6] [INFO] [1777834496.506551717] [aic_engine]: score: 1

[aic_engine-6] [INFO] [1777834496.506553347] [aic_engine]: message: Model validation succeeded.

[aic_engine-6] [INFO] [1777834496.506554987] [aic_engine]: tier_2:

[aic_engine-6] [INFO] [1777834496.506556327] [aic_engine]: score: 0

[aic_engine-6] [INFO] [1777834496.506590269] [aic_engine]: trial_3:

[aic_engine-6] [INFO] [1777834496.506591999] [aic_engine]: tier_1:

[aic_engine-6] [INFO] [1777834496.506593639] [aic_engine]: score: 1

[aic_engine-6] [INFO] [1777834496.506595670] [aic_engine]: message: Model validation succeeded.

[aic_engine-6] [INFO] [1777834496.506597580] [aic_engine]: tier_2:

[aic_engine-6] [INFO] [1777834496.506608910] [aic_engine]: score: -1.0658234005719205

[aic_engine-6] [INFO] [1777834496.506610811] [aic_engine]: message: Scoring succeeded.

[aic_engine-6] [INFO] [1777834496.506612241] [aic_engine]: categories:

[aic_engine-6] [INFO] [1777834496.506613651] [aic_engine]: contacts:

[aic_engine-6] [INFO] [1777834496.506615701] [aic_engine]: score: 0

[aic_engine-6] [INFO] [1777834496.506617221] [aic_engine]: message: No contact detected.

[aic_engine-6] [INFO] [1777834496.506619101] [aic_engine]: duration:

[aic_engine-6] [INFO] [1777834496.506620471] [aic_engine]: score: 0

[aic_engine-6] [INFO] [1777834496.506622391] [aic_engine]: message: “Task duration: 160.00 seconds.”

[aic_engine-6] [INFO] [1777834496.506624911] [aic_engine]: insertion force:

[aic_engine-6] [INFO] [1777834496.506626702] [aic_engine]: score: -12

[aic_engine-6] [INFO] [1777834496.506629012] [aic_engine]: message: “Insertion force above 20.00 N, detected for a time of 34.84 seconds. Max detected force: 55.27N. This is above the threshold of 1.00 seconds. Penalty applied.”

[aic_engine-6] [INFO] [1777834496.506631322] [aic_engine]: trajectory efficiency:

[aic_engine-6] [INFO] [1777834496.506632832] [aic_engine]: score: 6

[aic_engine-6] [INFO] [1777834496.506634392] [aic_engine]: message: “Total end-effector path length: 0.00 m, initial plug-port distance: 0.32 m”

[aic_engine-6] [INFO] [1777834496.506637252] [aic_engine]: trajectory smoothness:

[aic_engine-6] [INFO] [1777834496.506639062] [aic_engine]: score: 4.9341765994280795

[aic_engine-6] [INFO] [1777834496.506640542] [aic_engine]: message: “Average linear jerk magnitude of the end effector: 8.88 m/s^3”

[aic_engine-6] [INFO] [1777834496.506642733] [aic_engine]: tier_3:

[aic_engine-6] [INFO] [1777834496.506644093] [aic_engine]: score: 48.346663438985772

[aic_engine-6] [INFO] [1777834496.506645613] [aic_engine]: message: Partial insertion detected with distance of 0.00m.

Here is my high level setup

  1. collected 160 episodes worth of training data using CheatCode with about 10 episodes per prompt
Insert sfp module into sfp_port_0 on nic_card_mount_2
Insert sfp module into sfp_port_0 on nic_card_mount_4
Insert sfp module into sfp_port_1 on nic_card_mount_3
Insert sfp module into sfp_port_1 on nic_card_mount_4
Insert sfp module into sfp_port_1 on nic_card_mount_2
Insert sfp module into sfp_port_0 on nic_card_mount_3
Insert sfp module into sfp_port_0 on nic_card_mount_1
Insert sfp module into sfp_port_1 on nic_card_mount_0
Insert sfp module into sfp_port_1 on nic_card_mount_1
Insert sfp module into sfp_port_0 on nic_card_mount_0
Insert SC plug into sc_port_base on sc_port_0
Insert SC plug into sc_port_base on sc_port_1
  1. Details of the dataset https://huggingface.co/datasets/pranavsaroha/aic-cheatcode-ds-v1-smoke

Demonstrations were generated using the CheatCode policy (ground-truth TF transforms). v1 dataset adds force/torque sensor data, joint velocities/efforts, contact flag, structured task metadata, and uses the commanded pose target as the action signal. Here was the setup:

FPS: 20

Cameras: left, center, right (256x288, AV1)

Action space: absolute Cartesian pose target = [pose.x, pose.y, pose.z, pose.qx, pose.qy, pose.qz, pose.qw]

State space (46 dims):

  • TCP pose (7): position xyz + quaternion xyzw

  • TCP velocity (6): linear xyz + angular xyz

  • Joint positions (7)

  • Joint velocities (7)

  • Joint efforts (7)

  • Wrist wrench (6): force xyz + torque xyz (post-tare)

  • F/T tare offset (6): the bias removed by the tare service

  1. Train the pi0.5, SmolVLA and ACT models using leRobot

python -m lerobot.scripts.lerobot_train \

--policy.type=pi05 \

--policy.pretrained_path=lerobot/pi05_base \

--policy.push_to_hub=false \

--policy.max_state_dim=46 \

--policy.max_action_dim=7 \

--policy.dtype=bfloat16 \

--policy.gradient_checkpointing=true \

--policy.train_expert_only=true \

--policy.use_relative_actions=false \

--policy.image_resolution=‘[224,224]’ \

--dataset.repo_id=pranavsaroha/ \

--output_dir=outputs/pi05_aic_v1 \

--batch_size=8 \

--steps=30000 \

--eval_freq=5000 \

--save_freq=2000 \

--log_freq=50 \

--seed=42 \

--wandb.enable=true \

I am looking for guidance on ways to improve the accuracy.

  1. From this thread, looks like I need to scale up 100 episodes per prompt (around 1,000 episodes total). Did that help?

  2. So far all my episodes are cheatCode generated, should I generate using tele-operation? If so, any guidance on the best way to go about this. Are you doing teleoperation with Gazebo, isaacSim or both?

  3. Does image resolution matter, currently cheatCode is generating 256×288

Any other model architectures tried? @jlamperez @bha51

thanks!

1 Like

@jlamperez @Pranav_Saroha how did your policies finally do?

I have created this thread for gathering learnings from the qualification phase please give that a go and share any insights if you can. Thanks.

1 Like

Hi @Pranav_Saroha , @Robin_Tomar

Sorry for not writing sooner, but I’ve been quite busy testing and understanding different options for the challenge

Thanks for sharing your setup. Here are my findings from the challenge in case they’re useful for you and others. I finished around 127 points (position 55), so I didn’t pass the cutoff, but I learned a lot along the way.

Started with pi0.5, switched to ACT

I initially used pi0.5. The problem was the cloud evaluation timings: model loading took ~2 seconds, there was no warmup (so the first inference was very slow), and subsequent inferences took ~200ms. With the strict configuration/loading timeouts in the cloud eval, I couldn’t make it work reliably. That’s why I moved to ACT.

The joint state ordering bug

Early on, my ACT model was completely ignoring the images. The robot followed a fixed curved trajectory regardless of what was in front of it. It looked blind.

Setting joint_positions to all zeros made the robot start “seeing” again. Debugging this, I found the issue: the order of joints in ROS didn’t match the order LeRobot was using when normalizing the dataset. Some normalized values were going crazy, drowning out the visual signal.

ROS order I was getting:

['shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint',
 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint', 'gripper']

I had to remap to LeRobot’s expected order:

"joint_positions.0": joint_positions[3],  # Wrist 1
"joint_positions.1": joint_positions[6],  # Gripper
"joint_positions.2": joint_positions[1],  # Shoulder Lift
"joint_positions.3": joint_positions[0],  # Shoulder Pan
"joint_positions.4": joint_positions[2],  # Elbow
"joint_positions.5": joint_positions[4],  # Wrist 2
"joint_positions.6": joint_positions[5],  # Wrist 3

This was a critical fix. Worth checking normalization carefully.

The visibility issue with my early data

I spent two weeks recording trajectories where the target port wasn’t even visible from the first frame. Then I re-read the challenge description: “the target port will always be within the view of the robot’s cameras”. I had to restart data collection. Lesson learned: read the spec carefully before automating data collection.

Resolution matters

I started recording at 288x256 and only got partial insertion on trial 1. I doubled the resolution and could be able to insert completely. To answer your question, yes, resolution matters for me.

Identifying the trial configurations

Since the trial details weren’t given upfront, I used the Task object inside insert_cable to identify them experimentally. After a few submissions and inspecting task.port_type, task.port_name, and task.target_module_name, I figured out:

  • Trial 1: nic_card_mount_2 + sfp_port_0
  • Trial 2: nic_card_mount_4 + sfp_port_1
  • Trial 3: sc_port_1

I then trained one specialized model per trial:

if task.port_type == "sc" and task.target_module_name == "sc_port_1":
    model = self.model_sc
elif task.port_type == "sfp" and task.port_name == "sfp_port_0" \
     and task.target_module_name == "nic_card_mount_2":
    model = self.model_sfp_port0
elif task.port_type == "sfp" and task.port_name == "sfp_port_1" \
     and task.target_module_name == "nic_card_mount_4":
    model = self.model_sfp_port1

What worked for Trial 1

After collecting ~1000 episodes around the area I was generating data, I trained ACT for 50k steps with chunk_size=100 and n_obs_steps=1. I wasn’t expecting much, but I got 75 (full insertion) consistently on Trial 1. I have to say that this model sometimes achieved full insertions and other times it came close to the hole, so in the end this approach ended up penalizing me in the score.

Identifying task_board pose experimentally

For Trials 2 and 3 I needed to find the task_board’s actual position and orientation in the eval, which wasn’t given. Because I was running out of time to gather general data and I thought that focusing on one area would be better. I used an interesting trick:

If you train an ACT model on one configuration and then change the task_board to a different position, the model still follows roughly the same average trajectory. It gives more weight to joint states than to images. So the robot ends up at roughly the same place regardless of where the port actually is.

The cloud evaluation returns the final plug-port distance:

[aic_engine-6] [INFO] [1778918641.437966126] [aic_engine]:   tier_3:
[aic_engine-6] [INFO] [1778918641.437976856] [aic_engine]:     score: 0
[aic_engine-6] [INFO] [1778918641.437987216] [aic_engine]:     message: 
"No insertion detected. Final plug port distance: 0.41m."

By trying different task_board configurations locally and matching the resulting distance to what the cloud reported, I could narrow down where the eval was placing the board.

I then recorded targeted data around those configurations and trained new models.

The fundamental limitation: causal confusion

This is the most important takeaway. My models ended up being specialists, not generalists. They worked well when the board was in the configuration I trained them for, but as soon as you moved the board even slightly, the model would weight joint state more than visual input and execute a memorized trajectory.

This is causal confusion (de Haan et al., 2019): the model takes a shortcut by predicting actions from previous actions/state rather than truly using visual features. With chunk_size=100 and n_obs_steps=1, ACT is especially susceptible: it predicts 100 actions from a single observation and doesn’t re-observe during execution.

You can see the consequence in submissions: Trial 1 would give 75 one day and 25 the next.

What I would do differently

To answer your specific questions:

  1. More episodes: yes, more data helped but only up to a point. The bigger issue I think is variability. 500 episodes with wide pose variability could beat 2000 with narrow variability. The model needs poses diverse enough that it can’t memorize trajectories.
  2. Teleop vs CheatCode: I only used CheatCode. Teleop might generate more naturally diverse trajectories, but I can’t speak to that.
  3. Resolution: yes, going from 288x256 to higher resolution made a difference for me.
  4. Architecture changes I’d try next:
    • Lower chunk_size (25-50) so the model re-observes more often.
    • Higher n_obs_steps (3-5) to force temporal visual context.
    • Diffusion Policy instead of ACT (generalizes better with less data, less prone to causal confusion).
    • SmolVLA (compact VLA, fast loading unlike pi0.5).
    • Heavy image augmentation during training to prevent the model from relying on pixel-level shortcuts.

I didn’t have time to try Diffusion Policy or SmolVLA, which I think were the biggest missed opportunities on my side.

Hope this helps. Good luck!

Here’s a video showing how my three models behave in an environment similar to cloud simulation.

5 Likes

Thanks for sharing. This is really useful. I’ll try out some of these learnings. Just curious will you be working on this any further or dropping it now?

1 Like

Hi @jlamperez, really impressive results — especially the semantic behavior from the PI0.5 policy.
That final insertion phase you mentioned is honestly one of the hardest parts of real-world ROS deployment. In a lot of these setups, I’ve noticed the issue is often less about the policy quality itself, and more about stale state during execution:

  • camera pose updates arriving slightly late
  • TF drift during motion
  • inference latency causing actions to execute against an older world state
  • grasp/insertion commands being generated from observations that are already 10–50ms out of date
    The policy ends up “fighting” reality during the last few millimeters.

I am studying a measure that involves adding lightweight validation before performing insert operations:

  • rejecting commands if TF timestamps drift too far
  • checking short-term pose stability before insertion
  • gating actions if object pose variance spikes during approach

Not really changing the policy — more like protecting it from stale world-state assumptions.
Curious whether you’re handling this mostly through controller tuning, or if you’ve added any runtime checks/watchdogs around the insertion phase itself.
Really cool work either way — this is exactly where sim/demo success starts colliding with physical reality.