Train VLA Models with Self-Referential Rewards for Robotic Manipulation
Vision-Language-Action (VLA) models for robotics face extreme reward sparsity: tasks succeed or fail, with little feedback for in-between attempts. SRPO (Self-Referential Policy Optimization) breaks this bottleneck by using the model's own successful trajectories as self-reference. Failed attempts are measured against the model's successful ones from the same training batch, enabling dense progress-based rewards without external reward models or demonstrations.
This achieves 99.2% success on LIBERO (103% relative improvement from supervised baseline) by leveraging the model's latent world representation to assess behavioral progress robustly.
Core Concept
Standard VLA training for manipulation suffers from:
- Reward Sparsity: Most trajectories fail; binary success/failure provides no gradient signal for intermediate progress
- Sparse Demonstrations: Expert trajectories are expensive; RL without them requires dense rewards
- Domain Shift: Reward models trained on one set of tasks fail on new objects/scenes
SRPO addresses all three by enabling self-comparison: rather than comparing failed trajectories to fixed rewards or external demonstrations, the model compares its current attempt to its own successful trajectories (from the same batch). A latent world model captures progress via compressed state representations, enabling robust progress estimation without task-specific fine-tuning.
Architecture Overview
- Vision-Language-Action Model: Encoder (vision + language) → action decoder; generates manipulation actions
- Latent World Model: Encodes trajectories into latent state representations capturing progress
- Self-Referential Comparison: Compare failed trajectory's latent progression to successful trajectories' latent states
- Progress-Based Rewards: Measure behavioral advancement using latent space distances, not task-specific metrics
- Batch-Based Training: Successful and failed trajectories in same batch enable direct comparison
Implementation Steps
Step 1: World Model for State Representation. Encode trajectories into latent progress space.
import torch
import torch.nn as nn
class LatentWorldModel(nn.Module):
"""
Encodes manipulation trajectories into latent state representations.
Captures progress toward goals without task-specific knowledge.
"""
def __init__(self, observation_dim=512, action_dim=7, latent_dim=128):
super().__init__()
# Encoder: vision + action → latent state
self.encoder = nn.Sequential(
nn.Linear(observation_dim + action_dim, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, latent_dim)
)
# Predictor: predict next latent state from current + action
self.predictor = nn.Sequential(
nn.Linear(latent_dim + action_dim, 256),
nn.ReLU(),
nn.Linear(256, latent_dim)
)
self.latent_dim = latent_dim
def encode_trajectory(self, observations, actions):
"""
Encode trajectory into sequence of latent states.
observations: (traj_len, obs_dim)
actions: (traj_len, action_dim)
"""
latent_states = []
for t in range(len(observations)):
obs_action = torch.cat([observations[t], actions[t]], dim=0)
latent = self.encoder(obs_action)
latent_states.append(latent)
return torch.stack(latent_states) # (traj_len, latent_dim)
def predict_next_state(self, latent_state, action):
"""Predict next latent state (for training/debugging)."""
input_combined = torch.cat([latent_state, action], dim=-1)
next_latent = self.predictor(input_combined)
return next_latent
def compute_progress(self, failed_latent_traj, success_latent_traj):
"""
Measure progress of failed trajectory toward success.
Compares each step of failed trajectory to success trajectory.
"""
# Align trajectories (handle length mismatch)
min_len = min(len(failed_latent_traj), len(success_latent_traj))
distances = []
for t in range(min_len):
# L2 distance in latent space
dist = torch.norm(
failed_latent_traj[t] - success_latent_traj[t],
p=2
)
distances.append(dist)
# Average distance to success trajectory
avg_distance = torch.stack(distances).mean()
# Progress reward: negative distance (closer = better)
progress = -avg_distance
return progress
Step 2: Implement Self-Referential Comparison.
class SelfReferentialComparison:
"""
Compare failed trajectories to successful ones in the batch.
"""
def __init__(self, world_model):
self.world_model = world_model
def compute_self_referential_reward(self, failed_traj, success_trajs):
"""
Compute reward for failed trajectory based on self-comparison.
failed_traj: dict with 'observations' and 'actions'
success_trajs: list of successful trajectory dicts
"""
# Encode failed trajectory
failed_latent = self.world_model.encode_trajectory(
failed_traj['observations'],
failed_traj['actions']
)
# Encode successful trajectories
success_latents = []
for traj in success_trajs:
latent = self.world_model.encode_trajectory(
traj['observations'],
traj['actions']
)
success_latents.append(latent)
# Compare to each successful trajectory
progress_scores = []
for success_latent in success_latents:
progress = self.world_model.compute_progress(failed_latent, success_latent)
progress_scores.append(progress)
# Average progress across all successful trajectories
avg_progress = torch.stack(progress_scores).mean()
return avg_progress.item()
def compute_batch_rewards(self, trajectories):
"""
Process batch of trajectories, extracting successful and failed.
Compute self-referential rewards for each failed trajectory.
"""
successful = [t for t in trajectories if t['success']]
failed = [t for t in trajectories if not t['success']]
rewards = {}
for i, traj in enumerate(trajectories):
if traj['success']:
# Successful trajectories get positive baseline reward
rewards[i] = 1.0
else:
# Failed trajectories get self-referential progress reward
if successful:
progress_reward = self.compute_self_referential_reward(
traj,
successful
)
rewards[i] = progress_reward # Typically in [-1, 0] range
else:
# No successful trajectories in batch; use zero reward
rewards[i] = 0.0
return rewards
Step 3: VLA Model with SRPO Training.
class VisionLanguageActionModel(nn.Module):
"""
VLA model for manipulation: encodes vision + language → actions.
"""
def __init__(self, vision_encoder_dim=512, language_encoder_dim=256, action_dim=7):
super().__init__()
# Vision encoder (e.g., ViT or CNN)
self.vision_encoder = load_vision_encoder()
# Language encoder (e.g., BERT)
self.language_encoder = load_language_encoder()
# Action decoder
self.action_decoder = nn.Sequential(
nn.Linear(vision_encoder_dim + language_encoder_dim, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, action_dim)
)
def forward(self, observations, language_input):
"""
observations: (batch, seq_len, 3, H, W) or (batch, obs_features)
language_input: (batch, seq_len) tokenized language
Returns: actions (batch, seq_len, action_dim)
"""
# Encode observations
if len(observations.shape) == 5: # Image observations
batch, seq_len, _, _, _ = observations.shape
obs_flat = observations.reshape(batch * seq_len, 3, -1, -1)
vision_features = self.vision_encoder(obs_flat)
vision_features = vision_features.reshape(batch, seq_len, -1)
else:
vision_features = self.vision_encoder(observations)
# Encode language
lang_features = self.language_encoder(language_input) # (batch, lang_dim)
lang_features = lang_features.unsqueeze(1).expand_as(vision_features[:, :, :lang_features.shape[-1]])
# Combine and decode actions
combined = torch.cat([vision_features, lang_features], dim=-1)
actions = self.action_decoder(combined)
return actions
Step 4: SRPO Training Loop.
def train_vla_with_srpo(
vla_model, world_model, environment, num_episodes=10000,
vla_lr=1e-4, world_model_lr=1e-4
):
"""
Train VLA model with self-referential policy optimization.
"""
vla_optimizer = torch.optim.AdamW(vla_model.parameters(), lr=vla_lr)
world_optimizer = torch.optim.AdamW(world_model.parameters(), lr=world_model_lr)
comparator = SelfReferentialComparison(world_model)
for episode in range(num_episodes):
# Collect batch of trajectories
batch_trajs = []
for rollout in range(8): # 8 rollouts per batch
obs_list = []
action_list = []
reward = None
# Reset environment and get initial observation
obs, lang = environment.reset()
trajectory_success = False
# Rollout episode
for step in range(100):
obs_list.append(obs)
# VLA generates action
with torch.no_grad():
obs_tensor = torch.tensor(obs).unsqueeze(0)
lang_tensor = torch.tensor(lang).unsqueeze(0)
action = vla_model(obs_tensor, lang_tensor)[0].detach().numpy()
# Environment step
obs, done, task_success = environment.step(action)
action_list.append(action)
if task_success or done:
trajectory_success = task_success
break
batch_trajs.append({
'observations': obs_list,
'actions': action_list,
'success': trajectory_success
})
# ===== Compute SRPO Rewards =====
rewards = comparator.compute_batch_rewards(batch_trajs)
# ===== Update VLA Policy =====
vla_loss = 0
for i, traj in enumerate(batch_trajs):
# Reconstruct actions from VLA
obs_tensor = torch.tensor(traj['observations']).float()
lang_tensor = torch.tensor([lang] * len(traj['observations'])).unsqueeze(1)
with torch.enable_grad():
vla_actions = vla_model(obs_tensor, lang_tensor)
# Target actions (actual executed)
target_actions = torch.tensor(traj['actions']).float()
# MSE loss weighted by reward
action_loss = torch.nn.functional.mse_loss(vla_actions, target_actions)
weighted_loss = action_loss * rewards[i] # Reward weighting
vla_loss += weighted_loss
avg_vla_loss = vla_loss / len(batch_trajs)
vla_optimizer.zero_grad()
avg_vla_loss.backward()
torch.nn.utils.clip_grad_norm_(vla_model.parameters(), 1.0)
vla_optimizer.step()
# ===== Update World Model =====
# Train world model to improve trajectory encoding
world_loss = 0
for traj in batch_trajs:
obs_tensor = torch.tensor(traj['observations']).float()
action_tensor = torch.tensor(traj['actions']).float()
# Encode trajectory
latents = world_model.encode_trajectory(obs_tensor, action_tensor)
# Predict next states (reconstruction loss)
for t in range(len(latents) - 1):
pred_next = world_model.predict_next_state(latents[t], action_tensor[t])
reconstruction_loss = torch.nn.functional.mse_loss(pred_next, latents[t + 1])
world_loss += reconstruction_loss
avg_world_loss = world_loss / max(len(batch_trajs), 1)
world_optimizer.zero_grad()
avg_world_loss.backward()
torch.nn.utils.clip_grad_norm_(world_model.parameters(), 1.0)
world_optimizer.step()
if episode % 100 == 0:
success_rate = sum(1 for t in batch_trajs if t['success']) / len(batch_trajs)
print(f"Episode {episode}: success_rate={success_rate:.2%}, "
f"vla_loss={avg_vla_loss.item():.4f}, "
f"world_loss={avg_world_loss.item():.4f}")
Practical Guidance
When to Use: Robotic manipulation tasks with sparse rewards (pick-and-place, assembly); scenarios with few expert demonstrations.
Hyperparameters:
- Latent dimension: 128–256 depending on task complexity
- Batch size: ≥ 8 to ensure successful trajectories in batch for comparison
- Progress normalization: scale latent distances to [0, 1] range for stable training
Pitfalls:
- Successful trajectory requirements: If all trajectories fail, no self-reference possible; initialize with BC or weak RL first
- Latent space collapse: World model may learn trivial encodings; use reconstruction losses to maintain expressiveness
- Task-specific adaptation: Model may overfit to initial task distribution; use diverse task set
- Trajectory length mismatch: Align trajectories by timestep or use dynamic time-warping
When NOT to Use: Tasks with very sparse successes (< 10% baseline); single-task learning where diversity is low.
Integration: Works with any VLA backbone (OpenVLA, RT-2, etc.); requires environment interface for data collection.
Reference: https://arxiv.org/abs/2511.15605