Files
lerobot/src/lerobot/policies/wall_x/modeling_wall_x.py
T
Steven Palma 15724826dd chore: use alias & constants (#2785)
* chore: use alias and constants

* fix(rl): solve circular dependecy

* chore: nit right constant

* chore: pre-commit

* chore(script): conflict tokenizer train

---------

Signed-off-by: Steven Palma <imstevenpmwork@ieee.org>
2026-01-13 09:49:46 +01:00

2009 lines
87 KiB
Python

#!/usr/bin/env python
# Copyright 2025 HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Wall-X: Cross-embodiment robotic control using Qwen2.5-VL with flow matching.
[Paper](https://github.com/x2-robot/wall-x)
Install wall-x extra dependencies:
```bash
pip install -e ".[wall_x]"
```
Example of finetuning a wall-x model:
```bash
lerobot-train \
--policy.type=wall_x \
--dataset.repo_id=your/dataset \
--batch_size=32 \
--steps=100000
```
"""
import math
from collections import deque
from os import PathLike
from typing import Any
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from peft import LoraConfig, get_peft_model
from PIL import Image
from qwen_vl_utils.vision_process import smart_resize
from torch import Tensor
from torch.distributions import Beta
from torch.nn import CrossEntropyLoss
from torchdiffeq import odeint
from transformers import AutoProcessor, BatchFeature
from transformers.cache_utils import (
StaticCache,
)
from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import (
Qwen2_5_VLForConditionalGeneration,
)
from transformers.utils import is_torchdynamo_compiling, logging
from lerobot.policies.pretrained import PreTrainedPolicy
from lerobot.policies.utils import populate_queues
from lerobot.policies.wall_x.configuration_wall_x import WallXConfig
from lerobot.policies.wall_x.constant import (
GENERATE_SUBTASK_RATIO,
IMAGE_FACTOR,
MAX_PIXELS,
MIN_PIXELS,
MODEL_TYPE,
PRIORITY_ORDER,
RESOLUTION,
TOKENIZER_MAX_LENGTH,
)
from lerobot.policies.wall_x.qwen_model.configuration_qwen2_5_vl import Qwen2_5_VLConfig
from lerobot.policies.wall_x.qwen_model.qwen2_5_vl_moe import (
Qwen2_5_VisionTransformerPretrainedModel,
Qwen2_5_VLACausalLMOutputWithPast,
Qwen2_5_VLMoEModel,
)
from lerobot.policies.wall_x.utils import (
get_wallx_normal_text,
preprocesser_call,
process_grounding_points,
replace_action_token,
)
from lerobot.utils.constants import ACTION, OBS_STATE
logger = logging.get_logger(__name__)
class SinusoidalPosEmb(nn.Module):
"""Sinusoidal positional embedding for diffusion timesteps."""
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, x):
device = x.device
half_dim = self.dim // 2
emb = math.log(10000) / (half_dim - 1)
emb = torch.exp(torch.arange(half_dim, device=device) * -emb)
emb = x[:, None] * emb[None, :]
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
return emb
class ActionHead(nn.Module):
"""
Action prediction head with flow matching.
Implements Beta-distributed noise scheduling and temporal embeddings
for action sequence prediction.
"""
def __init__(self, config):
super().__init__()
self.config = config
self.action_dim = sum(config.dof_config.values())
self.propri_dim = sum(config.agent_pos_config.values())
self.hidden_size = config.hidden_size
# Beta distribution for noise scheduling
self.beta_alpha = 1.5
self.beta_beta = 1.0
self.s = 0.999
# Sinusoidal timestep embedding
self.time_embed = SinusoidalPosEmb(config.hidden_size)
# Action embedding network
# *2 for action + DOF mask concatenation
self.w1 = nn.Linear(self.action_dim * 2, self.hidden_size, bias=False)
self.w2 = nn.Linear(self.hidden_size * 2, self.hidden_size, bias=False) # *2 for action + time
self.w3 = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
self.act_fn = nn.SiLU()
# Project back to action space
self.action_proj_back = nn.Linear(self.hidden_size, self.action_dim, bias=False)
# Proprioception projection
self.propri_proj = nn.Linear(self.propri_dim * 2, self.hidden_size, bias=False)
def sample_time(self, batch_size, device):
"""Sample timesteps using Beta distribution (always in float32 for numerical stability)."""
beta_dist = Beta(
torch.tensor(self.beta_alpha, dtype=torch.float32, device=device),
torch.tensor(self.beta_beta, dtype=torch.float32, device=device),
)
sample = beta_dist.sample([batch_size])
time = (1 - sample) * self.s
return time
def forward(self, action_chunk, dof_mask=None):
"""
Process action sequences with noise injection for training.
Args:
action_chunk: Action sequences [batch, seq_len, action_dim]
dof_mask: DOF mask [batch, seq_len, action_dim]
Returns:
tuple: (action_embeddings, flow_target)
"""
batch_size = action_chunk.shape[0]
device = action_chunk.device
weight_dtype = self.w1.weight.dtype
# Sample time outside of autocast (Beta distribution needs float32)
time = self.sample_time(batch_size, device)
t = time.unsqueeze(-1).unsqueeze(-1)
# Noise and flow computation in float32
noise = torch.randn_like(action_chunk, dtype=torch.float32)
action_chunk_f32 = action_chunk.to(torch.float32)
noisy_action = (1 - t) * noise + t * action_chunk_f32
flow = action_chunk_f32 - noise
# Project noisy actions
if dof_mask is not None:
noisy_action = torch.cat([noisy_action, dof_mask.to(torch.float32)], dim=-1)
# Convert to weight dtype for linear layers
noisy_action = noisy_action.to(dtype=weight_dtype)
action_embed = self.w1(noisy_action)
# Generate time embeddings and combine
time_embed = self.time_embed(time)
time_embed = time_embed.unsqueeze(1).repeat(1, action_embed.shape[1], 1)
time_embed = time_embed.to(dtype=weight_dtype)
concat_embed = torch.cat([action_embed, time_embed], dim=-1)
concat_embed = self.w2(concat_embed)
embed = self.w3(self.act_fn(concat_embed))
return embed, flow
def step(self, timestep, noisy_action, dof_mask=None):
"""Single denoising step for inference."""
weight_dtype = self.w1.weight.dtype
if dof_mask is not None:
noisy_action = torch.cat([noisy_action, dof_mask], dim=-1)
noisy_action = noisy_action.to(dtype=weight_dtype)
time_embed = self.time_embed(timestep)
action_embed = self.w1(noisy_action)
time_embed = time_embed.unsqueeze(1).repeat(1, action_embed.shape[1], 1)
time_embed = time_embed.to(device=noisy_action.device, dtype=weight_dtype)
concat_embed = torch.cat([action_embed, time_embed], dim=-1)
concat_embed = self.w2(concat_embed)
embed = self.w3(self.act_fn(concat_embed))
return embed
def flow_loss(self, action_hidden_states, flow, dof_mask=None):
"""Compute flow matching loss (all computations in float32 for stability)."""
# Ensure all inputs are float32
action_hidden_states = action_hidden_states.to(torch.float32)
flow = flow.to(torch.float32)
action_pred = self.action_proj_back(action_hidden_states)
loss = F.mse_loss(action_pred, flow, reduction="none")
if dof_mask is not None:
dof_mask = dof_mask.reshape(-1, dof_mask.shape[-1]).to(torch.float32)
loss = loss * dof_mask
return loss
def proprioception_proj(self, proprioception, dof_mask=None, use_history=False):
"""Project proprioceptive data to hidden space."""
# Ensure proper device and dtype alignment
proprioception = proprioception.to(device=self.propri_proj.weight.device).to(
dtype=self.propri_proj.weight.dtype
)
if dof_mask is not None:
# Concatenate proprioception with DOF mask
# TODO: Use variable-based dimension checking for better flexibility
if use_history:
proprioception = torch.cat([proprioception, dof_mask], dim=-1)
else:
proprioception = torch.cat([proprioception, dof_mask], dim=-1)
proprioception = proprioception.to(device=self.propri_proj.weight.device).to(
dtype=self.propri_proj.weight.dtype
)
return self.propri_proj(proprioception)
class Qwen2_5_VLMoEForAction(Qwen2_5_VLForConditionalGeneration):
"""
Qwen2.5 Vision-Language Mixture of Experts model for action processing.
This model extends the base Qwen2.5 VL model with action token processing capabilities
and optional LoRA fine-tuning support.
"""
_tied_weights_keys = ["lm_head.weight"]
config_class = Qwen2_5_VLConfig
_no_split_modules = ["Qwen2_5_VLDecoderLayer_with_MoE", "Qwen2_5_VLVisionBlock"]
@classmethod
def from_pretrained(
cls,
pretrained_name_or_path,
config=None,
action_tokenizer_path=None,
attn_implementation: str = "eager",
cache_dir: str | PathLike | None = None,
force_download: bool = False,
local_files_only: bool = False,
token: str | bool | None = None,
revision: str = "main",
strict: bool = False,
**kwargs: Any,
):
"""
Load model from pretrained model path.
Args:
pretrained_model_path (str): Model directory path containing model.safetensors file
config_path (str, optional): Configuration file path, if None will look for qwen25_config.json in pretrained_model_path
action_tokenizer_path (str, optional): Action tokenizer path, if None will load from default config
attn_implementation (str, optional): Attention implementation, if None will load from default config
**kwargs: Additional arguments
Returns:
Qwen2_5_VLMoEForAction: Loaded model instance
"""
if config is None:
config = cls.config_class.from_pretrained(
pretrained_name_or_path,
cache_dir=cache_dir,
force_download=force_download,
local_files_only=local_files_only,
token=token,
revision=revision,
strict=strict,
**kwargs,
)
if attn_implementation is not None:
config._attn_implementation = attn_implementation
processor = AutoProcessor.from_pretrained(pretrained_name_or_path, use_fast=True)
if action_tokenizer_path is not None:
action_tokenizer = AutoProcessor.from_pretrained(action_tokenizer_path, trust_remote_code=True)
processor.action_processor = action_tokenizer
else:
action_tokenizer = None
# Initialize model with configuration and processor
model = cls(config, processor=processor, action_tokenizer=action_tokenizer, **kwargs)
# Resize token embeddings to match processor tokenizer vocabulary size
model.resize_token_embeddings(len(processor.tokenizer))
# Try to load the model.safetensors file
print(f"Loading model from: {pretrained_name_or_path}")
try:
from transformers.utils import cached_file
# Try safetensors first
resolved_file = cached_file(
pretrained_name_or_path,
"model.safetensors",
cache_dir=kwargs.get("cache_dir"),
force_download=kwargs.get("force_download", False),
resume_download=kwargs.get("resume_download"),
proxies=kwargs.get("proxies"),
use_auth_token=kwargs.get("use_auth_token"),
revision=kwargs.get("revision"),
local_files_only=kwargs.get("local_files_only", False),
)
from safetensors.torch import load_file
sd = load_file(resolved_file)
print("✓ Loaded state dict from model.safetensors")
except Exception as e:
print(f"Could not load state dict from remote files: {e}")
print("Returning model without loading pretrained weights")
return model
state_dict = {}
# filter normalizer statistic params
del_keys = []
for key in sd.keys():
if "action_preprocessor.normalizer" in key:
del_keys.append(key)
for key in del_keys:
del sd[key]
state_dict.update(sd)
model.load_state_dict(state_dict, strict=False)
return model
def __init__(
self,
config,
use_fast_tokenizer=False,
processor=None,
action_tokenizer=None,
action_mapper=None,
flow_loss_weight=1.0,
):
"""
Initialize the Qwen2.5 VLMoE model for action processing.
Args:
config: Model configuration
use_fast_tokenizer (bool): Whether to use fast tokenizer
processor: Text and image processor
action_tokenizer: Action-specific tokenizer
action_mapper: Action mapping utility
flow_loss_weight (float): Weight for flow loss computation
"""
super().__init__(config)
# Initialize vision transformer and language model components
self.visual = Qwen2_5_VisionTransformerPretrainedModel._from_config(config.vision_config)
self.model = Qwen2_5_VLMoEModel(config)
self.vocab_size = config.vocab_size
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
# Initialize loss function without reduction for channel-wise loss computation
self.loss_fct = CrossEntropyLoss(reduction="none")
self.flow_loss_weight = flow_loss_weight
self.use_fast_tokenizer = use_fast_tokenizer
self.processor = processor
self.action_tokenizer = action_tokenizer
# Define action token IDs
self.define_action_token_id()
# Cache for rope deltas
self.rope_deltas = None
# Initialize action preprocessor
self.action_preprocessor = ActionHead(config)
# Apply LoRA if specified in configuration
if hasattr(config, "use_lora") and config.use_lora:
self.add_lora(
r=config.lora_r,
lora_alpha=config.lora_alpha,
target_modules=config.lora_target_modules,
lora_dropout=config.lora_dropout,
)
# Initialize weights and apply final processing
self.post_init()
def to_bfloat16_for_selected_params(self):
self.to(dtype=torch.bfloat16)
params_to_keep_float32 = []
for name, param in self.named_parameters():
if "input_layernorm" in name or "post_attention_layernorm" in name or "model.norm" in name:
params_to_keep_float32.append(name)
if "action_preprocessor" in name:
params_to_keep_float32.append(name)
for name, param in self.named_parameters():
if name in params_to_keep_float32:
param.data = param.data.to(torch.float32)
def define_action_token_id(self):
"""
Define action token IDs based on tokenizer configuration.
Creates mappings for fast action tokens, proprioception tokens, and general action tokens.
"""
# Create list of fast action token IDs
fast_action_token_list = []
if self.use_fast_tokenizer:
for i in range(self.processor.tokenizer.init_kwargs["action_token_vocab_size"]):
action_token_id = self.processor.tokenizer.convert_tokens_to_ids(f"<|action_token_{i}|>")
fast_action_token_list.append(action_token_id)
# Get special action token IDs
action_token_id = self.processor.tokenizer.convert_tokens_to_ids("<|action|>")
propri_token_id = self.processor.tokenizer.convert_tokens_to_ids("<|propri|>")
# Store action token ID mappings
self.action_token_id_set = {
"fast_action_token_list": fast_action_token_list,
"propri_token_id": propri_token_id,
"action_token_id": action_token_id,
}
def add_lora(self, r=8, lora_alpha=32, target_modules=["q_proj", "v_proj"], lora_dropout=0.1):
"""
Add LoRA (Low-Rank Adaptation) adapters to the model.
Args:
r (int): Rank of adaptation
lora_alpha (int): LoRA scaling parameter
target_modules (list): List of module names to apply LoRA to
lora_dropout (float): Dropout probability for LoRA layers
"""
config = LoraConfig(
r=r,
lora_alpha=lora_alpha,
target_modules=target_modules,
lora_dropout=lora_dropout,
bias="none",
task_type="CAUSAL_LM",
)
self.model = get_peft_model(self.model, config)
# Print information about trainable parameters
self.model.print_trainable_parameters()
def get_input_embeddings(self):
"""Get input embeddings layer."""
return self.model.embed_tokens
def set_input_embeddings(self, value):
"""Set input embeddings layer."""
self.model.embed_tokens = value
def get_output_embeddings(self):
"""Get output embeddings layer."""
return self.lm_head
def set_output_embeddings(self, new_embeddings):
"""Set output embeddings layer."""
self.lm_head = new_embeddings
def set_decoder(self, decoder):
"""Set the decoder model."""
self.model = decoder
def get_decoder(self):
"""Get the decoder model."""
return self.model
def get_rope_index(
self,
input_ids: torch.LongTensor | None = None,
image_grid_thw: torch.LongTensor | None = None,
video_grid_thw: torch.LongTensor | None = None,
second_per_grid_ts: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Calculate 3D RoPE (Rotary Position Embedding) indices for vision and text tokens.
This method computes position embeddings that account for the temporal, height, and width
dimensions of vision tokens (images/videos) while maintaining standard 1D position embeddings
for text tokens.
For vision tokens, 3D position embeddings are calculated based on:
- Temporal dimension: Time patches in videos
- Height dimension: Vertical patches in images/video frames
- Width dimension: Horizontal patches in images/video frames
For text tokens, standard 1D position embeddings are used, continuing from the maximum
vision position ID plus 1.
Args:
input_ids (torch.LongTensor, optional): Input token IDs of shape (batch_size, sequence_length)
image_grid_thw (torch.LongTensor, optional): Image grid dimensions (num_images, 3) for [temporal, height, width]
video_grid_thw (torch.LongTensor, optional): Video grid dimensions (num_videos, 3) for [temporal, height, width]
second_per_grid_ts (torch.Tensor, optional): Time interval per temporal grid (num_videos,)
attention_mask (torch.Tensor, optional): Attention mask (batch_size, sequence_length)
Returns:
tuple:
- position_ids (torch.LongTensor): 3D position IDs of shape (3, batch_size, sequence_length)
- mrope_position_deltas (torch.Tensor): Position deltas for mRoPE of shape (batch_size, 1)
"""
spatial_merge_size = self.config.vision_config.spatial_merge_size
image_token_id = self.config.image_token_id
video_token_id = self.config.video_token_id
vision_start_token_id = self.config.vision_start_token_id
mrope_position_deltas = []
if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None):
total_input_ids = input_ids
if attention_mask is None:
attention_mask = torch.ones_like(total_input_ids)
# Initialize 3D position IDs tensor
position_ids = torch.ones(
3,
input_ids.shape[0],
input_ids.shape[1],
dtype=input_ids.dtype,
device=input_ids.device,
)
image_index, video_index = 0, 0
attention_mask = attention_mask.to(total_input_ids.device)
# Process each sequence in the batch
for i, input_ids in enumerate(total_input_ids):
input_ids = input_ids[attention_mask[i] == 1]
image_nums, video_nums = 0, 0
# Find vision tokens and count images/videos
vision_start_indices = torch.argwhere(input_ids == vision_start_token_id).squeeze(1)
vision_tokens = input_ids[vision_start_indices + 1]
image_nums = (vision_tokens == image_token_id).sum()
video_nums = (vision_tokens == video_token_id).sum()
input_tokens = input_ids.tolist()
llm_pos_ids_list: list = []
st = 0
remain_images, remain_videos = image_nums, video_nums
# Process each vision token (image or video)
for _ in range(image_nums + video_nums):
# Find next image or video token
if image_token_id in input_tokens and remain_images > 0:
ed_image = input_tokens.index(image_token_id, st)
else:
ed_image = len(input_tokens) + 1
if video_token_id in input_tokens and remain_videos > 0:
ed_video = input_tokens.index(video_token_id, st)
else:
ed_video = len(input_tokens) + 1
# Determine if processing image or video token
if ed_image < ed_video:
# Process image token
t, h, w = (
image_grid_thw[image_index][0],
image_grid_thw[image_index][1],
image_grid_thw[image_index][2],
)
second_per_grid_t = 0
image_index += 1
remain_images -= 1
ed = ed_image
else:
# Process video token
t, h, w = (
video_grid_thw[video_index][0],
video_grid_thw[video_index][1],
video_grid_thw[video_index][2],
)
if second_per_grid_ts is not None:
second_per_grid_t = second_per_grid_ts[video_index]
else:
second_per_grid_t = 1.0
video_index += 1
remain_videos -= 1
ed = ed_video
# Calculate grid dimensions after spatial merging
llm_grid_t, llm_grid_h, llm_grid_w = (
t.item(),
h.item() // spatial_merge_size,
w.item() // spatial_merge_size,
)
text_len = ed - st
# Add position IDs for text tokens before vision token
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
# Calculate 3D position embeddings for vision tokens
range_tensor = torch.arange(llm_grid_t).view(-1, 1)
expanded_range = range_tensor.expand(-1, llm_grid_h * llm_grid_w)
# Calculate temporal position IDs with time scaling
time_tensor = (
expanded_range * second_per_grid_t * self.config.vision_config.tokens_per_second
)
time_tensor_long = time_tensor.long()
t_index = time_tensor_long.flatten()
# Calculate spatial position IDs
h_index = (
torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten()
)
w_index = (
torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten()
)
# Add 3D position IDs for vision tokens
llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
st = ed + llm_grid_t * llm_grid_h * llm_grid_w
# Add position IDs for remaining text tokens
if st < len(input_tokens):
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
text_len = len(input_tokens) - st
llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
# Concatenate all position IDs for this sequence
llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device)
mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i]))
mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1)
return position_ids, mrope_position_deltas
else:
# Handle case without vision tokens - use standard 1D position embeddings
if attention_mask is not None:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device)
max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0]
mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1]
else:
position_ids = (
torch.arange(input_ids.shape[1], device=input_ids.device)
.view(1, 1, -1)
.expand(3, input_ids.shape[0], -1)
)
mrope_position_deltas = torch.zeros(
[input_ids.shape[0], 1],
device=input_ids.device,
dtype=input_ids.dtype,
)
return position_ids, mrope_position_deltas
def train_step_forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: list[torch.FloatTensor] | None = None,
inputs_embeds: torch.FloatTensor | None = None,
moe_token_types: torch.LongTensor | None = None, # MoE token type assignments
labels: torch.LongTensor | None = None,
use_cache: bool | None = None,
output_attentions: bool | None = None,
output_hidden_states: bool | None = None,
return_dict: bool | None = None,
pixel_values: torch.Tensor | None = None,
pixel_values_videos: torch.FloatTensor | None = None,
image_grid_thw: torch.LongTensor | None = None,
video_grid_thw: torch.LongTensor | None = None,
action_chunk: torch.FloatTensor | None = None, # Action trajectory chunks
proprioception: torch.FloatTensor | None = None, # Joint position/orientation data
rope_deltas: torch.LongTensor | None = None,
cache_position: torch.LongTensor | None = None,
second_per_grid_ts: torch.Tensor | None = None,
dof_mask: torch.FloatTensor | None = None,
agent_pos_mask: torch.FloatTensor | None = None,
**kwargs,
) -> tuple | Qwen2_5_VLACausalLMOutputWithPast:
"""
Forward pass for training with multi-modal inputs including vision, text, and action data.
This method handles the complete forward pass during training, processing various input modalities
including images, videos, text, proprioceptive data, and action sequences. It computes losses
for both language modeling and action prediction using flow matching.
Args:
input_ids (torch.LongTensor, optional): Input token IDs
attention_mask (torch.Tensor, optional): Attention mask for input tokens
position_ids (torch.LongTensor, optional): Position IDs for tokens
past_key_values (List[torch.FloatTensor], optional): Cached key-value pairs for generation
inputs_embeds (torch.FloatTensor, optional): Pre-computed input embeddings
moe_token_types (torch.LongTensor, optional): Token type assignments for MoE routing
labels (torch.LongTensor, optional): Target labels for loss computation
use_cache (bool, optional): Whether to use key-value caching
output_attentions (bool, optional): Whether to return attention weights
output_hidden_states (bool, optional): Whether to return hidden states
return_dict (bool, optional): Whether to return structured output
pixel_values (torch.Tensor, optional): Image pixel values
pixel_values_videos (torch.FloatTensor, optional): Video pixel values
image_grid_thw (torch.LongTensor, optional): Image grid dimensions (temporal, height, width)
video_grid_thw (torch.LongTensor, optional): Video grid dimensions (temporal, height, width)
action_chunk (torch.FloatTensor, optional): Action trajectory data chunks
proprioception (torch.FloatTensor, optional): Proprioceptive sensor data (joint positions, etc.)
rope_deltas (torch.LongTensor, optional): RoPE position deltas
cache_position (torch.LongTensor, optional): Cache position indices
second_per_grid_ts (torch.Tensor, optional): Time interval per temporal grid
dof_mask (torch.FloatTensor, optional): Degrees of freedom mask for action tokens
agent_pos_mask (torch.FloatTensor, optional): Agent position mask for proprioceptive data
**kwargs: Additional keyword arguments
Returns:
Union[Tuple, Qwen2_5_VLACausalLMOutputWithPast]: Model outputs including losses, logits,
and auxiliary information, or tuple if return_dict=False
"""
batch_size, seq_length = input_ids.shape
# Set output configuration from model config if not specified
output_attentions = (
output_attentions if output_attentions is not None else self.config.output_attentions
)
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Calculate RoPE position IDs if not provided
# Note: Cannot calculate rope deltas with 4D attention mask. TODO: Fix this limitation
if position_ids is None and (attention_mask is None or attention_mask.ndim == 2):
# Calculate RoPE index once per generation in the pre-fill stage only
if (
(cache_position is not None and cache_position[0] == 0)
or self.rope_deltas is None
or (past_key_values is None or past_key_values.get_seq_length() == 0)
):
position_ids, rope_deltas = self.get_rope_index(
input_ids,
image_grid_thw,
video_grid_thw,
second_per_grid_ts,
attention_mask,
)
self.rope_deltas = rope_deltas
# Use previously calculated rope deltas to get correct position IDs
else:
delta = (
(cache_position[0] + self.rope_deltas).to(self.device)
if cache_position is not None
else 0
)
position_ids = torch.arange(seq_length, device=self.device)
position_ids = position_ids.view(1, -1).expand(batch_size, -1)
if cache_position is not None: # otherwise `deltas` is an int `0`
delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0)
position_ids = position_ids.add(delta)
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
# Process input embeddings with multi-modal data
if inputs_embeds is None:
inputs_embeds = self.model.embed_tokens(input_ids)
# Process image embeddings
if pixel_values is not None:
pixel_values = pixel_values.type(self.visual.dtype)
image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw)
mask = input_ids == self.config.image_token_id
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
image_mask = mask_expanded.to(inputs_embeds.device)
image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
# Process video embeddings
if pixel_values_videos is not None:
pixel_values_videos = pixel_values_videos.type(self.visual.dtype)
video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw)
n_video_tokens = (input_ids == self.config.video_token_id).sum().item()
n_video_features = video_embeds.shape[0]
# Validate video token and feature count match
if n_video_tokens != n_video_features:
raise ValueError(
f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}"
)
mask = input_ids == self.config.video_token_id
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
video_mask = mask_expanded.to(inputs_embeds.device)
video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds)
# Process proprioceptive data (joint positions, orientations, etc.)
if proprioception is not None:
proprioception = proprioception.to(inputs_embeds.device).to(inputs_embeds.dtype)
agent_pos_mask = agent_pos_mask.to(inputs_embeds.device).to(inputs_embeds.dtype)
proprioception = self.action_preprocessor.proprioception_proj(
proprioception,
agent_pos_mask,
use_history=proprioception.shape[1] > 1,
)
mask = input_ids == self.action_token_id_set["propri_token_id"]
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
proprioception_mask = mask_expanded.to(inputs_embeds.device)
proprioception = proprioception.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(proprioception_mask, proprioception)
elif self.training:
# Dummy forward pass to ensure gradient registration in DDP
# This handles cases where one process has proprioception data while another doesn't
# Without this, DDP would hang waiting for a gradient that will never be computed
dummy_input = torch.randn(
2,
self.action_preprocessor.propri_dim * 2,
device=inputs_embeds.device,
)
dummy_forward = self.action_preprocessor.proprioception_proj(dummy_input)
dummy_loss = sum(p.sum() for p in dummy_forward)
inputs_embeds = inputs_embeds + 0 * dummy_loss
# Process action chunk data
if action_chunk is not None:
action_chunk = action_chunk.to(inputs_embeds.device).to(inputs_embeds.dtype)
dof_mask = dof_mask.to(inputs_embeds.device).to(inputs_embeds.dtype)
noisy_action_emb, flow = self.action_preprocessor(action_chunk, dof_mask)
mask = input_ids == self.action_token_id_set["action_token_id"]
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
action_mask = mask_expanded.to(inputs_embeds.device)
noisy_action_emb = noisy_action_emb.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(action_mask, noisy_action_emb)
if attention_mask is not None:
attention_mask = attention_mask.to(inputs_embeds.device)
# Forward pass through the main model
outputs = self.model(
input_ids=None,
position_ids=position_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
moe_token_types=moe_token_types, # Pass token types for MoE routing
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
hidden_states = outputs[0]
hidden_states = hidden_states.to(self.lm_head.weight.dtype)
logits = self.lm_head(hidden_states)
# Initialize loss computation variables
loss = None
cross_entropy_loss, flow_loss = None, None
channel_loss_dict = None
channel_loss_count_dict = None
# Compute losses if labels are provided
if labels is not None:
loss = torch.tensor(0.0, device=hidden_states.device, dtype=torch.float32)
# Compute standard cross-entropy loss for language modeling
shift_logits = logits[..., :-1, :].contiguous().to(torch.float32)
shift_labels = labels[..., 1:].contiguous()
shift_logits = shift_logits.view(-1, self.config.vocab_size)
shift_labels = shift_labels.view(-1)
# Enable model parallelism by moving labels to correct device
shift_labels = shift_labels.to(shift_logits.device)
non_ignored_mask = shift_labels != -100
_cross_entropy_loss = self.loss_fct(shift_logits, shift_labels)
cross_entropy_loss = (
_cross_entropy_loss[non_ignored_mask].mean()
if non_ignored_mask.any()
else torch.tensor(0.0, device=shift_logits.device, dtype=torch.float32)
)
# Add cross-entropy loss to total loss if valid
if not torch.isnan(cross_entropy_loss):
loss = loss + cross_entropy_loss.to(torch.float32)
else:
with torch.no_grad():
cross_entropy_loss.detach()
if action_chunk is not None:
action_mask = input_ids == self.action_token_id_set["action_token_id"]
if action_mask.any():
action_hidden_states = hidden_states[action_mask].to(torch.float32)
flow = flow.reshape(-1, flow.shape[-1]).to(torch.float32)
_flow_loss = self.action_preprocessor.flow_loss(action_hidden_states, flow, dof_mask)
if isinstance(_flow_loss, torch.Tensor):
flow_loss = _flow_loss.mean()
if loss is not None:
loss = loss + self.flow_loss_weight * flow_loss.to(torch.float32)
else:
loss = self.flow_loss_weight * flow_loss.to(torch.float32)
_flow_loss = _flow_loss.view(dof_mask.shape[0], dof_mask.shape[1], dof_mask.shape[2])
# Return outputs based on return_dict setting
if not return_dict:
output = (logits,) + outputs[1:]
return (loss,) + output if loss is not None else output
return Qwen2_5_VLACausalLMOutputWithPast(
loss=loss,
cross_entropy_loss=(cross_entropy_loss.clone() if cross_entropy_loss is not None else None),
flow_loss=flow_loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
rope_deltas=self.rope_deltas,
channel_loss_dict=channel_loss_dict,
channel_loss_count_dict=channel_loss_count_dict,
)
def predict_action(self, predict_mode: str, **kwargs):
"""
Predict actions using specified prediction mode.
Args:
predict_mode (str): Prediction mode, either "fast" or "diffusion"
**kwargs: Additional arguments passed to the predict method
Returns:
tuple: (predicted_action, ground_truth_action) where ground_truth_action may be None
"""
assert predict_mode in ["fast", "diffusion"]
output = self.predict(predict_mode=predict_mode, **kwargs)
return output["predict_action"], output.get("gt_action", None)
@torch.no_grad()
def predict(
self,
predict_mode: str,
pred_horizon: int | None = None,
action_dim: int | None = None,
input_ids: torch.LongTensor = None,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: list[torch.FloatTensor] | None = None,
inputs_embeds: torch.FloatTensor | None = None,
moe_token_types: torch.LongTensor | None = None,
labels: torch.LongTensor | None = None,
use_cache: bool | None = None,
output_attentions: bool | None = None,
output_hidden_states: bool | None = None,
return_dict: bool | None = None,
pixel_values: torch.Tensor | None = None,
pixel_values_videos: torch.FloatTensor | None = None,
image_grid_thw: torch.LongTensor | None = None,
video_grid_thw: torch.LongTensor | None = None,
action_chunk: torch.FloatTensor | None = None,
proprioception: torch.FloatTensor | None = None,
rope_deltas: torch.LongTensor | None = None,
cache_position: torch.LongTensor | None = None,
second_per_grid_ts: torch.Tensor | None = None,
num_inference_timesteps: int | None = 10,
dof_mask: torch.FloatTensor | None = None,
agent_pos_mask: torch.FloatTensor | None = None,
re_generate: bool = False,
**kwargs,
):
"""
Multi-modal prediction method supporting text generation, fast action prediction, and diffusion-based action prediction.
This method handles three prediction modes:
1. "text": Pure text generation using autoregressive decoding
2. "fast": Fast action prediction using discrete action tokens
3. "diffusion": Continuous action prediction using diffusion/flow matching
Args:
predict_mode (str): Prediction mode ("text", "fast", or "diffusion")
pred_horizon (int, optional): Prediction horizon for action sequences
action_dim (int, optional): Dimensionality of action space
input_ids (torch.LongTensor, optional): Input token IDs
attention_mask (torch.Tensor, optional): Attention mask for input tokens
position_ids (torch.LongTensor, optional): Position IDs for tokens
past_key_values (List[torch.FloatTensor], optional): Cached key-value pairs
inputs_embeds (torch.FloatTensor, optional): Pre-computed input embeddings
moe_token_types (torch.LongTensor, optional): Token type assignments for MoE routing
labels (torch.LongTensor, optional): Target labels for evaluation
use_cache (bool, optional): Whether to use key-value caching
output_attentions (bool, optional): Whether to return attention weights
output_hidden_states (bool, optional): Whether to return hidden states
return_dict (bool, optional): Whether to return structured output
pixel_values (torch.Tensor, optional): Image pixel values
pixel_values_videos (torch.FloatTensor, optional): Video pixel values
image_grid_thw (torch.LongTensor, optional): Image grid dimensions
video_grid_thw (torch.LongTensor, optional): Video grid dimensions
action_chunk (torch.FloatTensor, optional): Ground truth action sequences
proprioception (torch.FloatTensor, optional): Proprioceptive sensor data
rope_deltas (torch.LongTensor, optional): RoPE position deltas
cache_position (torch.LongTensor, optional): Cache position indices
second_per_grid_ts (torch.Tensor, optional): Time interval per temporal grid
num_inference_timesteps (int, optional): Number of diffusion inference steps
dof_mask (torch.FloatTensor, optional): Degrees of freedom mask
agent_pos_mask (torch.FloatTensor, optional): Agent position mask
re_generate (bool, optional): Whether to use sampling for regeneration
**kwargs: Additional keyword arguments
Returns:
dict: Dictionary containing prediction results with keys like:
- 'predict_action': Predicted action sequences
- 'gt_action': Ground truth actions (if available)
- 'input_text': Input text (for text/fast modes)
- 'predict_output_text': Generated text (for text/fast modes)
- 'gt_output_text': Ground truth text (for text/fast modes)
"""
batch_size = input_ids.shape[0] if input_ids is not None else inputs_embeds.shape[0]
# Text and fast modes require batch size 1 for autoregressive generation
if predict_mode in ["text", "fast"]:
assert batch_size == 1, "predict only support batch size 1 for ar generation"
# Set output configuration from model config if not specified
output_attentions = (
output_attentions if output_attentions is not None else self.config.output_attentions
)
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Process input embeddings with multi-modal data
if inputs_embeds is None:
inputs_embeds = self.model.embed_tokens(input_ids)
# Process image embeddings
if pixel_values is not None:
pixel_values = pixel_values.type(self.visual.dtype)
image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw)
n_image_tokens = (input_ids == self.config.image_token_id).sum().item()
n_image_features = image_embeds.shape[0]
# Validate image token and feature count match
if n_image_tokens != n_image_features:
raise ValueError(
f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}"
)
mask = input_ids == self.config.image_token_id
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
image_mask = mask_expanded.to(inputs_embeds.device)
image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
# Process video embeddings
if pixel_values_videos is not None:
pixel_values_videos = pixel_values_videos.type(self.visual.dtype)
video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw)
n_video_tokens = (input_ids == self.config.video_token_id).sum().item()
n_video_features = video_embeds.shape[0]
# Validate video token and feature count match
if n_video_tokens != n_video_features:
raise ValueError(
f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}"
)
mask = input_ids == self.config.video_token_id
mask_unsqueezed = mask.unsqueeze(-1)
mask_expanded = mask_unsqueezed.expand_as(inputs_embeds)
video_mask = mask_expanded.to(inputs_embeds.device)
video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds)
# Process proprioceptive data
if proprioception is not None:
proprioception = proprioception.to(inputs_embeds.device).to(inputs_embeds.dtype)
agent_pos_mask = agent_pos_mask.to(inputs_embeds.device).to(inputs_embeds.dtype)
proprio_embed = self.action_preprocessor.proprioception_proj(
proprioception,
agent_pos_mask,
use_history=proprioception.shape[1] > 1,
)
proprioception_mask = input_ids == self.action_token_id_set["propri_token_id"]
proprio_embed = proprio_embed.to(torch.bfloat16)
inputs_embeds[proprioception_mask] = proprio_embed.reshape(-1, inputs_embeds.shape[-1])
if attention_mask is not None:
attention_mask = attention_mask.to(inputs_embeds.device)
# Calculate RoPE position IDs if not provided
# Note: Cannot calculate rope deltas with 4D attention mask. TODO: Fix this limitation
if position_ids is None and (attention_mask is None or attention_mask.ndim == 2):
# Calculate RoPE index once per generation in the pre-fill stage only
if (
(cache_position is not None and cache_position[0] == 0)
or self.rope_deltas is None
or (past_key_values is None or past_key_values.get_seq_length() == 0)
):
position_ids, rope_deltas = self.get_rope_index(
input_ids,
image_grid_thw,
video_grid_thw,
second_per_grid_ts,
attention_mask,
)
self.rope_deltas = rope_deltas
# Use previously calculated rope deltas to get correct position IDs
else:
batch_size, seq_length, _ = inputs_embeds.shape
delta = (
(cache_position[0] + self.rope_deltas).to(inputs_embeds.device)
if cache_position is not None
else 0
)
position_ids = torch.arange(seq_length, device=inputs_embeds.device)
position_ids = position_ids.view(1, -1).expand(batch_size, -1)
if cache_position is not None: # otherwise `deltas` is an int `0`
delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0)
position_ids = position_ids.add(delta)
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
# Prepare action chunk data if provided
if action_chunk is not None:
action_chunk = action_chunk.to(inputs_embeds.device).to(torch.float32)
output = {}
# Split input sequence for text and fast modes (not needed for diffusion)
if predict_mode == "text" or predict_mode == "fast":
# Look for generation prompt tokens: <|im_start|>assistant
generation_prompt_ids = torch.tensor(
[151644, 77091], device=input_ids.device, dtype=input_ids.dtype
)
matches = (input_ids[0, :-1] == generation_prompt_ids[0]) & (
input_ids[0, 1:] == generation_prompt_ids[1]
)
if matches.any():
split_pos = torch.nonzero(matches, as_tuple=True)[0][0].item()
# Extract ground truth output tokens (including newline)
gt_output_ids = input_ids[:, split_pos + 3 :]
# Remove output part from input, keeping prompt
input_ids = input_ids[:, : split_pos + 3]
inputs_embeds = inputs_embeds[:, : split_pos + 3, :]
if attention_mask is not None:
attention_mask = attention_mask[:, : split_pos + 3]
if labels is not None:
labels = labels[:, split_pos + 3 :]
else:
raise ValueError(
"input_ids does not contain the generation prompt tokens <|im_start|>assistant"
)
# Decode input text for output
input_text = self.processor.batch_decode(
input_ids, skip_special_tokens=False, clean_up_tokenization_spaces=True
)
output["input_text"] = input_text
# Handle text and fast prediction modes using autoregressive generation
if predict_mode == "text" or predict_mode == "fast":
# Initialize MoE token types for generation
moe_token_types = torch.zeros_like(input_ids)
batch = {
"input_ids": input_ids,
"attention_mask": attention_mask,
"pixel_values": pixel_values,
"moe_token_types": moe_token_types,
"image_grid_thw": image_grid_thw,
"dof_mask": dof_mask,
"agent_pos_mask": agent_pos_mask,
"proprioception": proprioception,
}
# Generate output tokens
predict_output_ids = self.generate(
**batch,
max_new_tokens=100,
eos_token_id=[self.processor.tokenizer.eos_token_id],
use_cache=True,
pad_token_id=self.processor.tokenizer.pad_token_id,
temperature=(1.0 if not re_generate else 0.7), # Higher temperature for regeneration
do_sample=(False if not re_generate else True), # Enable sampling for regeneration
)
# Decode generated and ground truth text
gt_output_text = self.processor.batch_decode(
gt_output_ids,
skip_special_tokens=False,
clean_up_tokenization_spaces=True,
)
predict_output_text = self.processor.batch_decode(
predict_output_ids,
skip_special_tokens=False,
clean_up_tokenization_spaces=True,
)
output["gt_output_text"] = gt_output_text
output["predict_output_text"] = predict_output_text
# Convert tokens to actions for fast prediction mode
if predict_mode == "fast":
action_id = []
# Extract action tokens from generated sequence
for token_id_i in predict_output_ids[0]:
if token_id_i.item() >= self.processor.tokenizer.init_kwargs["action_token_start_index"]:
action_id.append(
token_id_i.item() - self.processor.tokenizer.init_kwargs["action_token_start_index"]
)
predict_action = self.processor.action_processor.decode(
[action_id], time_horizon=pred_horizon, action_dim=action_dim
)
# Handle action decoding errors
if np.sum(predict_action) == 0:
print("Error in decoding action, predict_action is None")
output["predict_action"] = None
else:
# Convert discrete tokens to continuous actions
predict_action = torch.tensor(predict_action, device=self.device)
dof_mask = dof_mask.to(self.device).to(pixel_values.dtype)
# removed unnormalization step for now
predict_action = predict_action[:, :, dof_mask[0, 0, :].bool()]
output["predict_action"] = predict_action
# Process ground truth actions if available
if action_chunk is not None:
# Apply DOF mask to get ground truth actions
# removed unnormalization step for now
action_chunk = action_chunk[:, :, dof_mask[0, 0, :].bool()]
output["gt_action"] = action_chunk
else:
output["gt_action"] = None
# Handle diffusion-based action prediction
if predict_mode == "diffusion":
# Initialize with random noise
noisy_action = torch.randn(
size=(batch_size, pred_horizon, action_dim),
dtype=torch.float32,
device=inputs_embeds.device,
)
dof_mask = dof_mask.to(inputs_embeds.device).to(torch.float32)
def step(timestep, noisy_action):
"""
Single denoising step for diffusion process.
Args:
timestep: Current diffusion timestep
noisy_action: Current noisy action estimate
Returns:
torch.Tensor: Predicted clean action
"""
action_mask = input_ids == self.action_token_id_set["action_token_id"]
assert action_mask.any(), "No action token found in input_ids"
# Prepare timestep for batch processing
timestep = timestep.unsqueeze(0).repeat(noisy_action.shape[0])
action_embed = self.action_preprocessor.step(
timestep=timestep, noisy_action=noisy_action, dof_mask=dof_mask
)
action_embed = action_embed.reshape(-1, inputs_embeds.shape[-1])
# Ensure action_embed has the correct dtype and device before assignment
action_embed = action_embed.to(dtype=inputs_embeds.dtype, device=inputs_embeds.device)
# Create temporary copy of embeddings (clone preserves dtype)
temp_inputs_embeds = inputs_embeds.clone()
temp_inputs_embeds[action_mask] = action_embed
# Forward pass through transformer
transformer_outputs = self.model(
input_ids=None,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=temp_inputs_embeds,
moe_token_types=moe_token_types,
use_cache=True,
output_attentions=False,
output_hidden_states=False,
return_dict=True,
)
# Extract action predictions from hidden states
hidden_states = transformer_outputs.last_hidden_state
action_mask = input_ids == self.action_token_id_set["action_token_id"]
action_hidden_states = hidden_states[action_mask].to(torch.float32)
pred = self.action_preprocessor.action_proj_back(action_hidden_states)
return pred.reshape(batch_size, pred_horizon, action_dim)
# Perform ODE integration for diffusion sampling
times = torch.linspace(
0,
1,
num_inference_timesteps + 1,
device=inputs_embeds.device,
dtype=torch.float32,
)
action_trajectory = odeint(step, noisy_action, times, method="euler")
# Extract final predicted action
# Removed unnormalization step for now
predict_action = action_trajectory[-1]
output["predict_action"] = predict_action
# Process ground truth actions if available
# removed unnormalization step for now
if action_chunk is not None:
output["gt_action"] = action_chunk[:, :, dof_mask[0, 0, :].bool()]
return output
def forward(self, mode: str | None = None, predict_mode: str | None = "text", **kwargs):
"""
Main forward pass dispatcher for different execution modes.
This method routes execution to appropriate forward functions based on the specified mode:
- No mode (None): Training step with gradient disabled
- 'predict': Prediction/inference mode
- 'train': Training mode with gradients enabled
- 'validate': Validation mode with gradients disabled
Args:
mode (str, optional): Execution mode. If None, defaults to training step without gradients
predict_mode (str, optional): Prediction mode for 'predict' mode ("text", "fast", or "diffusion")
**kwargs: Additional arguments passed to the selected forward function
Returns:
Model outputs appropriate for the selected mode
Todo:
- Add support for distinguishing multi-modal data types in prediction mode
"""
if not mode:
with torch.no_grad():
return self.train_step_forward(**kwargs)
elif mode == "predict":
return self.predict(predict_mode=predict_mode, **kwargs)
elif mode == "train":
return self.train_step_forward(use_cache=False, **kwargs)
elif mode == "validate":
with torch.no_grad():
return self.train_step_forward(use_cache=False, **kwargs)
else:
raise NotImplementedError("invalid key")
def prepare_inputs_for_generation(
self,
input_ids,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
moe_token_types=None,
cache_position=None,
position_ids=None,
use_cache=True,
pixel_values=None,
pixel_values_videos=None,
image_grid_thw=None,
video_grid_thw=None,
second_per_grid_ts=None,
proprioception=None,
dof_mask=None,
agent_pos_mask=None,
**kwargs,
):
"""
Prepare inputs for autoregressive generation with multi-modal support.
This method handles input preparation for generation, including proper slicing of inputs
based on cache position, MoE token type management, and multi-modal data handling.
Vision inputs are selectively forwarded only when needed during generation.
Args:
input_ids: Input token IDs
past_key_values: Cached key-value pairs from previous generation steps
attention_mask: Attention mask for input tokens
inputs_embeds: Pre-computed input embeddings
moe_token_types: Token type assignments for MoE routing
cache_position: Current cache position for generation
position_ids: Position IDs for tokens
use_cache: Whether to use key-value caching
pixel_values: Image pixel values
pixel_values_videos: Video pixel values
image_grid_thw: Image grid dimensions
video_grid_thw: Video grid dimensions
second_per_grid_ts: Time interval per temporal grid
proprioception: Proprioceptive sensor data
dof_mask: Degrees of freedom mask
agent_pos_mask: Agent position mask
**kwargs: Additional arguments
Returns:
dict: Prepared model inputs for generation step
Todo:
- Test this function thoroughly with various input configurations
Note:
This is an overridden method that handles specific cases for multi-modal generation:
- Slices input_ids through cache_position to keep only unprocessed tokens
- Handles special cases for input_embeds, generation methods, and GPU synchronization
- Manages vision inputs to avoid unnecessary forward passes
"""
# Initialize MoE token types if not provided
if moe_token_types is None:
moe_token_types = torch.zeros_like(
input_ids
) # FIXME: Handle case when input_embeds is used instead
else:
# Ensure moe_token_types length matches input_ids
if moe_token_types.shape[1] < input_ids.shape[1]:
# Calculate required padding length
pad_length = input_ids.shape[1] - moe_token_types.shape[1]
# Create padding tensor with default token type (0)
pad_tensor = torch.zeros(
(moe_token_types.shape[0], pad_length),
dtype=moe_token_types.dtype,
device=moe_token_types.device,
)
# Concatenate padding to existing moe_token_types
moe_token_types = torch.cat([moe_token_types, pad_tensor], dim=1)
# Handle input slicing based on cache state and special cases
if past_key_values is not None:
if inputs_embeds is not None and input_ids.shape[1] == 0: # Exception 4: input_embeds case
inputs_embeds = inputs_embeds[:, -cache_position.shape[0] :]
moe_token_types = moe_token_types[:, -cache_position.shape[0] :]
elif inputs_embeds is not None or ( # Exception 1: input_embeds provided
is_torchdynamo_compiling() or cache_position[-1] >= input_ids.shape[1]
): # Exception 3: GPU sync edge case
input_ids = input_ids[:, -cache_position.shape[0] :]
moe_token_types = moe_token_types[:, -cache_position.shape[0] :]
elif input_ids.shape[1] != cache_position.shape[0]: # Default case (Exception 2 is no-op)
cache_pos = cache_position.clone()
input_ids = input_ids[:, cache_pos]
moe_token_types = moe_token_types[:, cache_pos]
# Skip vision inputs for continuation steps (not initial generation)
if cache_position[0] != 0:
pixel_values = None
pixel_values_videos = None
# Determine whether to use inputs_embeds or input_ids for this generation step
if inputs_embeds is not None and len(cache_position) == inputs_embeds.shape[1]:
model_inputs = {"inputs_embeds": inputs_embeds, "input_ids": None}
else:
model_inputs = {"input_ids": input_ids, "inputs_embeds": None}
# Prepare 4D causal attention mask for static cache
if isinstance(past_key_values, StaticCache) and attention_mask.ndim == 2:
if model_inputs["inputs_embeds"] is not None:
batch_size, sequence_length, _ = inputs_embeds.shape
device = inputs_embeds.device
else:
batch_size, sequence_length = input_ids.shape
device = input_ids.device
attention_mask = self.model._prepare_4d_causal_attention_mask_with_cache_position(
attention_mask,
sequence_length=sequence_length,
target_length=past_key_values.get_max_cache_shape(),
dtype=self.lm_head.weight.dtype,
device=device,
cache_position=cache_position,
batch_size=batch_size,
config=self.config,
past_key_values=past_key_values,
)
# Assemble all model inputs for generation
model_inputs.update(
{
"position_ids": position_ids,
"past_key_values": past_key_values,
"moe_token_types": moe_token_types,
"use_cache": use_cache,
"attention_mask": attention_mask,
"pixel_values": pixel_values,
"pixel_values_videos": pixel_values_videos,
"image_grid_thw": image_grid_thw,
"video_grid_thw": video_grid_thw,
"cache_position": cache_position,
"second_per_grid_ts": second_per_grid_ts,
"proprioception": proprioception,
"dof_mask": dof_mask,
"agent_pos_mask": agent_pos_mask,
}
)
return model_inputs
def _get_image_nums_and_video_nums(
self,
input_ids: torch.LongTensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Get the number of images and videos for each sample to calculate tensor separation lengths.
These parameters are computed directly from input_ids rather than being passed through
the processor to avoid unpredictable impacts from interface modifications.
Args:
input_ids (torch.LongTensor): Input token IDs of shape (batch_size, sequence_length)
Returns:
tuple:
- image_nums (torch.LongTensor): Number of images per sample
- video_nums (torch.LongTensor): Number of videos per sample
"""
image_token_id = self.config.image_token_id
video_token_id = self.config.video_token_id
vision_start_token_id = self.config.vision_start_token_id
# Find vision start tokens and their following tokens
vision_start_mask = input_ids == vision_start_token_id
vision_first_mask = torch.roll(vision_start_mask, shifts=1, dims=1)
image_mask = input_ids == image_token_id
video_mask = input_ids == video_token_id
# Count images and videos following vision start tokens
image_nums = torch.sum(vision_first_mask & image_mask, dim=1)
video_nums = torch.sum(vision_first_mask & video_mask, dim=1)
return image_nums, video_nums
def _expand_inputs_for_generation(
self,
expand_size: int = 1,
is_encoder_decoder: bool = False,
input_ids: torch.LongTensor | None = None,
**model_kwargs,
) -> tuple[torch.LongTensor, dict[str, Any]]:
"""
Expand inputs for generation with support for multi-modal tensors.
This is an overridden method that supports expanding tensors without a standard batch
size dimension, specifically for vision-related tensors:
- pixel_values.shape[0] = sum(sequence_lengths for all image samples)
- image_grid_thw.shape[0] = sum(num_images for all samples)
- Similar patterns for video tensors
Args:
expand_size (int): Factor by which to expand inputs (for beam search, etc.)
is_encoder_decoder (bool): Whether using encoder-decoder architecture
input_ids (torch.LongTensor, optional): Input token IDs
**model_kwargs: Additional model arguments to expand
Returns:
tuple: (expanded_input_ids, expanded_model_kwargs)
"""
if expand_size == 1:
return input_ids, model_kwargs
# Define keys for vision-related tensors that need special handling
visual_keys = [
"pixel_values",
"image_grid_thw",
"pixel_values_videos",
"video_grid_thw",
"second_per_grid_ts",
]
def _expand_dict_for_generation_visual(dict_to_expand):
"""Expand vision-related tensors based on image/video counts per sample."""
image_grid_thw = model_kwargs.get("image_grid_thw", None)
video_grid_thw = model_kwargs.get("video_grid_thw", None)
image_nums, video_nums = self._get_image_nums_and_video_nums(input_ids)
def _repeat_interleave_samples(x, lengths, repeat_times):
"""Split tensor by lengths and repeat each sample."""
samples = torch.split(x, lengths)
repeat_args = [repeat_times] + [1] * (x.dim() - 1)
result = torch.cat([sample.repeat(*repeat_args) for sample in samples], dim=0)
return result
for key in dict_to_expand:
if key == "pixel_values":
# Split images into samples and compute sequence lengths
samples = torch.split(image_grid_thw, list(image_nums))
lengths = [torch.prod(sample, dim=1).sum() for sample in samples]
dict_to_expand[key] = _repeat_interleave_samples(
dict_to_expand[key], lengths=lengths, repeat_times=expand_size
)
elif key == "image_grid_thw":
# Expand based on number of images per sample
lengths = list(image_nums)
dict_to_expand[key] = _repeat_interleave_samples(
dict_to_expand[key], lengths=lengths, repeat_times=expand_size
)
elif key == "pixel_values_videos":
# Split videos into samples and compute sequence lengths
samples = torch.split(video_grid_thw, list(video_nums))
lengths = [torch.prod(sample, dim=1).sum() for sample in samples]
dict_to_expand[key] = _repeat_interleave_samples(
dict_to_expand[key], lengths=lengths, repeat_times=expand_size
)
elif key == "video_grid_thw":
# Expand based on number of videos per sample
lengths = list(video_nums)
dict_to_expand[key] = _repeat_interleave_samples(
dict_to_expand[key], lengths=lengths, repeat_times=expand_size
)
elif key == "second_per_grid_ts":
# Handle list-type temporal grid data
if not isinstance(dict_to_expand[key], list):
raise TypeError(
f"Expected value for key '{key}' to be a list, but got {type(dict_to_expand[key])} instead."
)
tensor = torch.tensor(dict_to_expand[key])
lengths = list(video_nums)
tensor = _repeat_interleave_samples(tensor, lengths=lengths, repeat_times=expand_size)
dict_to_expand[key] = tensor.tolist()
return dict_to_expand
def _expand_dict_for_generation(dict_to_expand):
"""Expand standard tensors using repeat_interleave."""
for key in dict_to_expand:
if (
key != "cache_position"
and dict_to_expand[key] is not None
and isinstance(dict_to_expand[key], torch.Tensor)
and key not in visual_keys
):
dict_to_expand[key] = dict_to_expand[key].repeat_interleave(expand_size, dim=0)
return dict_to_expand
# Expand visual inputs only if input_ids is available for counting images/videos
# If input_ids is unavailable, visual inputs won't be used, so no expansion needed
if input_ids is not None and input_ids.numel() != 0:
model_kwargs = _expand_dict_for_generation_visual(model_kwargs)
# Expand input_ids using standard repeat_interleave
if input_ids is not None:
input_ids = input_ids.repeat_interleave(expand_size, dim=0)
# Expand all other model arguments
model_kwargs = _expand_dict_for_generation(model_kwargs)
# Handle encoder-decoder specific expansion
if is_encoder_decoder:
if model_kwargs.get("encoder_outputs") is None:
raise ValueError(
"If `is_encoder_decoder` is True, make sure that `encoder_outputs` is defined."
)
model_kwargs["encoder_outputs"] = _expand_dict_for_generation(model_kwargs["encoder_outputs"])
return input_ids, model_kwargs
class WallXPolicy(PreTrainedPolicy):
"""
Wall-X policy for cross-embodiment robotic control.
Integrates Qwen2.5-VL vision-language model with action prediction
using flow matching for continuous action spaces.
"""
config_class = WallXConfig
name = "wall_x"
def __init__(self, config: WallXConfig, **kwargs):
super().__init__(config)
config.validate_features()
self.config = config
# Initialize the wall-x model
self.model = Qwen2_5_VLMoEForAction.from_pretrained(
pretrained_name_or_path=config.pretrained_name_or_path,
action_tokenizer_path=config.action_tokenizer_path,
attn_implementation=config.attn_implementation,
)
self.model.to(config.device)
self.model.to_bfloat16_for_selected_params()
self.reset()
def reset(self):
"""Reset action queue."""
self._queues = {
ACTION: deque(maxlen=self.config.n_action_steps),
}
def get_optim_params(self):
"""Get parameters for optimization."""
return self.parameters()
def preprocess_inputs(
self,
batch: dict[str, Any],
) -> BatchFeature:
"""
Convert a batch of LeRobot dataset items to Wall-X model input format.
This processes a batched dictionary where tensors have batch dimension first.
Args:
batch: Dictionary with batched tensors:
- "observation.state": (batch_size, state_dim) or (batch_size, n_obs_steps, state_dim)
- "action": (batch_size, chunk_size, action_dim)
- "observation.images.<key>": (batch_size, C, H, W)
- "task": List[str] of length batch_size
Returns:
BatchFeature containing batched model inputs
"""
use_fast_tokenizer = self.config.use_fast_tokenizer
# Get batch size from state tensor
batch_size = batch[OBS_STATE].shape[0]
# ==================== PROCESS ALL SAMPLES ====================
all_image_inputs = []
all_texts = []
# Find image keys in batch
img_keys = [key for key in self.config.image_features if key in batch]
for i in range(batch_size):
# Vision preprocessing per sample
processed_frames = []
orig_height, orig_width = None, None
resized_height, resized_width = None, None
for key in img_keys:
current_obs = batch[key][i].clone() # (C, H, W)
if current_obs.dim() == 3:
current_obs = current_obs.permute(1, 2, 0) # (H, W, C)
img_pil = Image.fromarray((current_obs * 255).to(torch.uint8).cpu().numpy())
orig_width, orig_height = img_pil.size
target_size = RESOLUTION
if target_size != -1:
if orig_width > orig_height:
new_width = target_size
new_height = int(target_size * orig_height / orig_width)
else:
new_height = target_size
new_width = int(target_size * orig_width / orig_height)
img_pil = img_pil.resize((new_width, new_height))
current_width, current_height = img_pil.size
resized_height, resized_width = smart_resize(
current_height,
current_width,
factor=IMAGE_FACTOR,
min_pixels=MIN_PIXELS,
max_pixels=MAX_PIXELS,
)
resized_img = img_pil.resize((resized_width, resized_height))
processed_frames.append(resized_img)
all_image_inputs.append(processed_frames)
# Text preprocessing
task_text = batch["task"][i] if isinstance(batch["task"], list) else batch["task"]
instruction_info = {"instruction": task_text}
frame_index = batch["frame_index"][i] if "frame_index" in batch else 0
complete_text, _ = get_wallx_normal_text(
instruction_info,
self.config.chunk_size,
frame_index,
PRIORITY_ORDER,
img_keys,
generate_subtask_ratio=GENERATE_SUBTASK_RATIO,
)
text = process_grounding_points(
complete_text, orig_height, orig_width, resized_height, resized_width, MODEL_TYPE
)
all_texts.append(text)
# ==================== PROCESS AGENT POS ====================
agent_pos = batch[OBS_STATE] # (batch_size, state_dim)
if agent_pos.dim() == 2:
agent_pos = agent_pos.unsqueeze(1) # (batch_size, 1, state_dim)
agent_pos_mask = (~torch.isnan(agent_pos)).float()
agent_pos = agent_pos.nan_to_num(nan=0.0)
if agent_pos.shape[-1] != 20:
pad_size = 20 - agent_pos.shape[-1]
agent_pos = torch.cat(
[
agent_pos,
torch.zeros(agent_pos.shape[0], agent_pos.shape[1], pad_size, device=agent_pos.device),
],
dim=-1,
)
agent_pos_mask = torch.cat(
[
agent_pos_mask,
torch.zeros(
agent_pos_mask.shape[0],
agent_pos_mask.shape[1],
pad_size,
device=agent_pos_mask.device,
),
],
dim=-1,
)
# ==================== PROCESS ACTIONS ====================
action = batch.get(ACTION) # (batch_size, chunk_size, action_dim)
if action is not None:
if action.dim() == 2:
action = action.unsqueeze(1)
dof_mask = (~torch.isnan(action)).float()
action = action.nan_to_num(nan=0.0)
if action.shape[-1] != 20:
pad_size = 20 - action.shape[-1]
action = torch.cat(
[action, torch.zeros(action.shape[0], action.shape[1], pad_size, device=action.device)],
dim=-1,
)
dof_mask = torch.cat(
[
dof_mask,
torch.zeros(dof_mask.shape[0], dof_mask.shape[1], pad_size, device=dof_mask.device),
],
dim=-1,
)
else:
action_dim = self.config.output_features[ACTION].shape[0]
dof_mask = torch.cat(
[
torch.ones(
batch_size, self.config.chunk_size, action_dim, device=batch[OBS_STATE].device
),
torch.zeros(
batch_size, self.config.chunk_size, 20 - action_dim, device=batch[OBS_STATE].device
),
],
dim=-1,
)
# ==================== ACTION TOKEN REPLACEMENT ====================
all_texts = replace_action_token(
all_texts,
action,
self.model.action_tokenizer if use_fast_tokenizer else None,
dof_mask,
)
# ==================== TOKENIZATION ====================
inputs = preprocesser_call(
processor=self.model.processor,
text=all_texts,
images=all_image_inputs,
videos=None,
padding=True,
truncation=True,
return_tensors="pt",
max_length=TOKENIZER_MAX_LENGTH,
)
# ==================== ADDITIONAL INPUTS ====================
action_token_id = self.model.processor.tokenizer.convert_tokens_to_ids("<|action|>")
moe_token_types = inputs.input_ids == action_token_id
inputs["proprioception"] = agent_pos
inputs["agent_pos_mask"] = agent_pos_mask
inputs["action_chunk"] = action
inputs["dof_mask"] = dof_mask
inputs["moe_token_types"] = moe_token_types
inputs["frame_index"] = (
batch["frame_index"]
if "frame_index" in batch
else torch.zeros(batch_size, device=batch[OBS_STATE].device)
)
# Move all tensors to the correct device
device = self.config.device
for key, value in inputs.items():
if isinstance(value, torch.Tensor):
inputs[key] = value.to(device)
return inputs
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
"""
Training forward pass using Qwen2_5_VLMoEForAction.
Args:
batch: Dictionary containing preprocessed inputs from preprocess_inputs()
Expected keys: input_ids, attention_mask, pixel_values, image_grid_thw,
proprioception, agent_pos_mask, action_chunk, dof_mask, moe_token_types,
etc.
Returns:
tuple: (loss, loss_dict)
"""
batch = self.preprocess_inputs(
batch,
)
# Call the underlying model's forward with mode="train"
outputs = self.model(**batch, mode="train")
# Extract losses from output
loss = outputs.loss
loss_dict = {
"loss": loss.item() if loss is not None else 0.0,
}
if outputs.flow_loss is not None:
loss_dict["flow_loss"] = outputs.flow_loss.item()
if outputs.cross_entropy_loss is not None:
loss_dict["cross_entropy_loss"] = outputs.cross_entropy_loss.item()
# Add channel losses if available
if outputs.channel_loss_dict is not None:
for key, value in outputs.channel_loss_dict.items():
if isinstance(value, torch.Tensor):
loss_dict[f"channel_{key}"] = value.item()
return loss, loss_dict
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
"""Predict action chunk for evaluation."""
self.eval()
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
batch = self.preprocess_inputs(
batch,
)
if self.config.prediction_mode == "diffusion":
output = self.model(
**batch,
action_dim=self.config.max_action_dim,
pred_horizon=self.config.chunk_size,
mode="predict",
predict_mode="diffusion",
)
elif self.config.prediction_mode == "fast":
output = self.model(
**batch,
action_dim=self.config.output_features[ACTION].shape[0],
pred_horizon=self.config.chunk_size,
mode="predict",
predict_mode="fast",
)
else:
raise NotImplementedError(f"Prediction mode {self.config.prediction_mode} not implemented")
# Extract action tensor from output dictionary
actions = output["predict_action"]
# Unpad actions to actual action dimension
action_dim = self.config.output_features[ACTION].shape[0]
actions = actions[:, :, :action_dim]
return actions
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select single action for environment execution."""
self.eval()
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
# Use action queue
if len(self._queues[ACTION]) == 0:
actions = self.predict_action_chunk(batch)
self._queues[ACTION].extend(actions.transpose(0, 1)[: self.config.n_action_steps])
return self._queues[ACTION].popleft()