refactor(eo1): reuse shared VLA components (#4061)

This commit is contained in:
Steven Palma
2026-07-20 15:34:16 +02:00
committed by GitHub
parent f3c0707c5f
commit 76b67d6ca8
+14 -76
View File
@@ -18,7 +18,6 @@ from __future__ import annotations
import contextlib import contextlib
import logging import logging
import math
from collections import deque from collections import deque
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@@ -31,6 +30,8 @@ from torch import Tensor
from lerobot.utils.constants import ACTION, OBS_STATE from lerobot.utils.constants import ACTION, OBS_STATE
from lerobot.utils.import_utils import _transformers_available, require_package from lerobot.utils.import_utils import _transformers_available, require_package
from ..common.flow_matching import euler_integrate, sample_noise, sample_time_beta
from ..common.vla_utils import create_sinusoidal_pos_embedding, pad_vector
from ..pretrained import PreTrainedPolicy from ..pretrained import PreTrainedPolicy
from .configuration_eo1 import EO1Config from .configuration_eo1 import EO1Config
@@ -46,17 +47,6 @@ else:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def pad_vector(vector, new_dim):
"""Pad the last dimension of a vector to new_dim with zeros.
Can be (batch_size x sequence_length x features_dimension)
or (batch_size x features_dimension)
"""
if vector.shape[-1] >= new_dim:
return vector
return F.pad(vector, (0, new_dim - vector.shape[-1]))
class EO1Policy(PreTrainedPolicy): class EO1Policy(PreTrainedPolicy):
"""EO1 policy wrapper for LeRobot robot-only training/evaluation.""" """EO1 policy wrapper for LeRobot robot-only training/evaluation."""
@@ -136,47 +126,6 @@ class EO1Policy(PreTrainedPolicy):
return self.parameters() return self.parameters()
def get_safe_dtype(target_dtype, device_type):
"""Get a safe dtype for the given device type."""
if device_type == "mps" and target_dtype == torch.float64:
return torch.float32
if device_type == "cpu":
# CPU doesn't support bfloat16, use float32 instead
if target_dtype == torch.bfloat16:
return torch.float32
if target_dtype == torch.float64:
return torch.float64
return target_dtype
def create_sinusoidal_pos_embedding( # see openpi `create_sinusoidal_pos_embedding` (exact copy)
time: torch.Tensor, dimension: int, min_period: float, max_period: float, device="cpu"
) -> Tensor:
"""Computes sine-cosine positional embedding vectors for scalar positions."""
if dimension % 2 != 0:
raise ValueError(f"dimension ({dimension}) must be divisible by 2")
if time.ndim != 1:
raise ValueError("The time tensor is expected to be of shape `(batch_size, )`.")
dtype = get_safe_dtype(torch.float64, device.type)
fraction = torch.linspace(0.0, 1.0, dimension // 2, dtype=dtype, device=device)
period = min_period * (max_period / min_period) ** fraction
# Compute the outer product
scaling_factor = 1.0 / period * 2 * math.pi
sin_input = scaling_factor[None, :] * time[:, None]
return torch.cat([torch.sin(sin_input), torch.cos(sin_input)], dim=1)
def sample_beta(alpha, beta, bsize, device): # see openpi `sample_beta` (exact copy)
# Beta sampling uses _sample_dirichlet which isn't implemented for MPS, so sample on CPU
alpha_t = torch.tensor(alpha, dtype=torch.float32)
beta_t = torch.tensor(beta, dtype=torch.float32)
dist = torch.distributions.Beta(alpha_t, beta_t)
return dist.sample((bsize,)).to(device)
class EO1VisionActionProjector(torch.nn.Sequential): class EO1VisionActionProjector(torch.nn.Sequential):
"""This block implements the multi-layer perceptron (MLP) module.""" """This block implements the multi-layer perceptron (MLP) module."""
@@ -267,21 +216,17 @@ class EO1VisionFlowMatchingModel(nn.Module):
return func(*args, **kwargs) return func(*args, **kwargs)
def sample_noise(self, shape, device): def sample_noise(self, shape, device):
noise = torch.normal( return sample_noise(shape, device)
mean=0.0,
std=1.0,
size=shape,
dtype=torch.float32,
device=device,
)
return noise
def sample_time(self, bsize, device): def sample_time(self, bsize, device):
time_beta = sample_beta( return sample_time_beta(
self.config.time_sampling_beta_alpha, self.config.time_sampling_beta_beta, bsize, device bsize,
device,
alpha=self.config.time_sampling_beta_alpha,
beta=self.config.time_sampling_beta_beta,
scale=self.config.time_sampling_scale,
offset=self.config.time_sampling_offset,
) )
time = time_beta * self.config.time_sampling_scale + self.config.time_sampling_offset
return time.to(dtype=torch.float32, device=device)
def get_placeholder_mask( def get_placeholder_mask(
self, self,
@@ -587,18 +532,11 @@ class EO1VisionFlowMatchingModel(nn.Module):
(batch_size, chunk_size, self.config.max_action_dim), (batch_size, chunk_size, self.config.max_action_dim),
device, device,
).to(dtype=self.action_in_proj.weight.dtype) ).to(dtype=self.action_in_proj.weight.dtype)
dt = -1.0 / self.config.num_denoise_steps
past_key_values = outputs.past_key_values past_key_values = outputs.past_key_values
# 3. Denoise only the action chunk while keeping the prefix cache invariant. # 3. Denoise only the action chunk while keeping the prefix cache invariant.
for step in range(self.config.num_denoise_steps): def denoise_fn(input_x_t, current_timestep):
time = torch.full( action_time_embs = self.embed_suffix(current_timestep, input_x_t)
(batch_size,),
1.0 + step * dt,
device=device,
dtype=torch.float32,
)
action_time_embs = self.embed_suffix(time, x_t)
inputs_embeds[:, act_slice] = action_time_embs.to(inputs_embeds.dtype) inputs_embeds[:, act_slice] = action_time_embs.to(inputs_embeds.dtype)
# Keep the prefix KV cache invariant across denoising steps. # Keep the prefix KV cache invariant across denoising steps.
@@ -615,7 +553,7 @@ class EO1VisionFlowMatchingModel(nn.Module):
hidden_states = outputs.last_hidden_state[:, :chunk_size] hidden_states = outputs.last_hidden_state[:, :chunk_size]
hidden_states = hidden_states.to(dtype=self.action_out_proj.dtype) hidden_states = hidden_states.to(dtype=self.action_out_proj.dtype)
v_t = self.action_out_proj(hidden_states) v_t = self.action_out_proj(hidden_states)
return v_t.reshape(input_x_t.shape).to(input_x_t.dtype)
x_t += dt * v_t.reshape(x_t.shape) x_t = euler_integrate(denoise_fn, x_t, self.config.num_denoise_steps)
return x_t return x_t