mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-29 12:39:41 +00:00
2e9cd87bbd
* first commit * feat(policies): add VLA-JEPA * feat(policies): add VLA-JEPA * support vla_jepa * (feat)policies: add VLA-JEPA * linting * adding deps to pyproject.toml * updating uv lock * adding guards to avoid needing transformers and diffusers for type checking and basic tests * fixing action and state dim * fix warnings with qwen processor kwargs * fixing wm_loss not propagating * adjusting obs steps, tublets size to match original implementation * some more fixes to be closer to the original implem * adding more tests to ensure good coverage * align VLA-JEPA architecture with original checkpoint - Remove stale `action_num_heads` / `action_attention_head_dim` config fields; DiT head dimensions are now always derived from the preset (DiT-B/L/test). - Add `num_target_vision_tokens` and `action_max_seq_len` config fields required by the action head's future-token embedding and positional embedding tables. - Fix default `qwen_model_name` to 2B (matches all released checkpoints). - Rename `ActionEncoder` attrs w1/w2/w3 → layer1/layer2/layer3 to match checkpoint key names; replace `nn.Sequential` decoder/state-encoder with `_MLP2` (layer1/layer2 naming). - Fix `VLAJEPAActionHead` to size ActionEncoder and StateEncoder at `inner_dim` (DiT input width) rather than `action_hidden_size` (DiT output width). - Rename `DiT.blocks` → `transformer_blocks` and `attn` → `attn1` to match checkpoint; add alternating cross/self attention (even blocks cross-attend to Qwen context, odd blocks self-attend). - Add `DiT-test` preset for unit tests. - Rewrite `ActionConditionedVideoPredictor` with explicit ViT-style blocks (`_PredictorBlock` with fused qkv) to match checkpoint structure; rename `encoder`/`norm`/`proj` → `predictor_blocks`/`predictor_norm`/`predictor_proj`. * propagate action_is_pad masking through VLA-JEPA policy pipeline Pass the `action_is_pad` tensor from the batch through to the action head so padded timesteps are excluded from the flow-matching loss. * update VLA-JEPA tests for arch changes and action_is_pad - Switch conftest to use `action_model_type="DiT-test"` now that `action_num_heads` / `action_attention_head_dim` have been removed. - Add action_head tests covering fully-padded loss (zero) and equivalence of action_is_pad=None vs all-zeros mask. - Remove obsolete `test_native_to_lerobot_wm_only` test. * add VLA-JEPA documentation Covers architecture overview, pretrained checkpoints, config reference, training/eval commands for LIBERO-10, and guidance on fine-tuning for single-camera datasets. * add one-shot script to convert ginwind/VLA-JEPA checkpoints to safetensors (will remove once migrated) * make default params more aligned with paper and pretrained models - adding possibility of freezing qwen backbone and world model - added tests for weight loading * trying out to re-init the action head to avoid pretraining dimension mismatch * allow different state dim and action dim * removing missleading future_action_window_size to just use chunk_size * lots of changes to make existing weights work, need to massively refactor the pre and post processing * refactoring into using pre and post processor * pre-commit cleanup * fixing doc defaults args Signed-off-by: Maxime Ellerbach <maxime@ellerbach.net> * adressing dtype zeros issue * adding guard for diffusers * fixing training and exal examples * trying to close success rate gap * fix qwen norm layer output libero eval is now as expected * adding instructions for different embodiement + fixing some tests * smol fix to avoid having default CPU device when training * fixing misconception about multiview / singleview handling * removing conversion script * adding licences * adding .mdx docs and shortening polivy_vla_jepa_README.md * removing useless pre-processor * cleanup * removing swish in favor of silu * adding configuration gripper index and threshold * fixing simlink --------- Signed-off-by: Maxime Ellerbach <maxime@ellerbach.net> Co-authored-by: ginwind <ginwind@mail.ustc.edu.cn>
155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
# Copyright 2026 The 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from lerobot.configs.policies import PreTrainedConfig
|
|
from lerobot.configs.types import NormalizationMode
|
|
from lerobot.optim.optimizers import AdamWConfig
|
|
from lerobot.optim.schedulers import CosineDecayWithWarmupSchedulerConfig
|
|
|
|
|
|
@PreTrainedConfig.register_subclass("vla_jepa")
|
|
@dataclass
|
|
class VLAJEPAConfig(PreTrainedConfig):
|
|
n_obs_steps: int = 1
|
|
chunk_size: int = 7
|
|
n_action_steps: int = 7
|
|
|
|
normalization_mapping: dict[str, NormalizationMode] = field(
|
|
default_factory=lambda: {
|
|
"VISUAL": NormalizationMode.IDENTITY,
|
|
"STATE": NormalizationMode.MEAN_STD,
|
|
"ACTION": NormalizationMode.MIN_MAX,
|
|
}
|
|
)
|
|
|
|
qwen_model_name: str = "Qwen/Qwen3-VL-2B-Instruct"
|
|
jepa_encoder_name: str = "facebook/vjepa2-vitl-fpc64-256"
|
|
freeze_qwen: bool = False
|
|
enable_world_model: bool = True
|
|
# Enables cross-embodiment transfer: when fine-tuning a pretrained model on a robot with a
|
|
# different action or state dimensionality, the input/output projection layers must be
|
|
# re-initialised from scratch while the rest of the network keeps its pretrained weights.
|
|
# List the key prefixes that are allowed to have shape mismatches; anything else raises an error.
|
|
# e.g. ["model.action_model.action_encoder", "model.action_model.state_encoder"]
|
|
reinit_modules: list[str] | None = None
|
|
|
|
tokenizer_padding_side: str = "left"
|
|
prompt_template: str = "Your task is {instruction}. Infer the temporal dynamics from frames {actions} and produce the corresponding policy actions {e_actions}."
|
|
special_action_token: str = "<|action_{}|>"
|
|
embodied_action_token: str = "<|embodied_action|>"
|
|
|
|
action_dim: int = 7
|
|
state_dim: int = 8
|
|
|
|
num_action_tokens_per_timestep: int = 8
|
|
num_embodied_action_tokens_per_instruction: int = 32
|
|
num_inference_timesteps: int = 4
|
|
|
|
action_hidden_size: int = 1024
|
|
action_model_type: str = "DiT-B"
|
|
action_num_layers: int = 16
|
|
action_num_heads: int | None = None
|
|
action_attention_head_dim: int | None = None
|
|
action_dropout: float = 0.2
|
|
action_num_timestep_buckets: int = 1000
|
|
action_noise_beta_alpha: float = 1.5
|
|
action_noise_beta_beta: float = 1.0
|
|
action_noise_s: float = 0.999
|
|
num_target_vision_tokens: int = 32
|
|
action_max_seq_len: int = 1024
|
|
|
|
# total video frames loaded per sample
|
|
num_video_frames: int = 8
|
|
predictor_depth: int = 12
|
|
predictor_num_heads: int = 8
|
|
predictor_mlp_ratio: float = 4.0
|
|
predictor_dropout: float = 0.0
|
|
world_model_loss_weight: float = 0.1
|
|
jepa_tubelet_size: int = 2 # must match the encoder (e.g. 2 for vjepa2-vitl-fpc64-256)
|
|
repeated_diffusion_steps: int = 8 # independent noise draws per batch item (CogACT-style)
|
|
|
|
resize_images_to: tuple[int, int] | None = None
|
|
binarize_gripper_action: bool = True
|
|
pre_snap_gripper_action: bool = True
|
|
clip_normalized_actions: bool = True
|
|
gripper_dim: int = 6
|
|
gripper_threshold: float = 0.5
|
|
torch_dtype: str = "bfloat16"
|
|
|
|
optimizer_lr: float = 1e-4
|
|
optimizer_betas: tuple[float, float] = (0.9, 0.95)
|
|
optimizer_eps: float = 1e-8
|
|
optimizer_weight_decay: float = 1e-10
|
|
optimizer_grad_clip_norm: float = 10.0
|
|
scheduler_warmup_steps: int = 1_000
|
|
scheduler_decay_steps: int = 30_000
|
|
scheduler_decay_lr: float = 2.5e-6
|
|
|
|
def __post_init__(self) -> None:
|
|
super().__post_init__()
|
|
if self.freeze_qwen and self.enable_world_model:
|
|
# freezing qwen backbone makes world model training irrelevant since no grad flows
|
|
self.enable_world_model = False
|
|
if self.n_action_steps > self.chunk_size:
|
|
raise ValueError("`n_action_steps` must be <= `chunk_size`.")
|
|
if self.num_video_frames < 2 * self.jepa_tubelet_size:
|
|
raise ValueError(
|
|
f"`video_horizon` ({self.num_video_frames}) must be >= 2 * `jepa_tubelet_size` "
|
|
f"({self.jepa_tubelet_size}) to have at least one context and one GT temporal position."
|
|
)
|
|
|
|
def validate_features(self) -> None:
|
|
if not self.image_features:
|
|
raise ValueError("VLAJEPA requires at least one visual input feature.")
|
|
if self.action_feature is None:
|
|
raise ValueError("VLAJEPA requires an action output feature.")
|
|
self.action_dim = self.action_feature.shape[0]
|
|
if self.robot_state_feature is not None:
|
|
self.state_dim = self.robot_state_feature.shape[0]
|
|
|
|
def get_optimizer_preset(self) -> AdamWConfig:
|
|
return AdamWConfig(
|
|
lr=self.optimizer_lr,
|
|
betas=self.optimizer_betas,
|
|
eps=self.optimizer_eps,
|
|
weight_decay=self.optimizer_weight_decay,
|
|
grad_clip_norm=self.optimizer_grad_clip_norm,
|
|
)
|
|
|
|
def get_scheduler_preset(self) -> CosineDecayWithWarmupSchedulerConfig:
|
|
return CosineDecayWithWarmupSchedulerConfig(
|
|
peak_lr=self.optimizer_lr,
|
|
decay_lr=self.scheduler_decay_lr,
|
|
num_warmup_steps=self.scheduler_warmup_steps,
|
|
num_decay_steps=self.scheduler_decay_steps,
|
|
)
|
|
|
|
@property
|
|
def observation_delta_indices(self) -> list[int]:
|
|
# load video_horizon frames starting from current timestep: [t, t+1, ..., t+video_horizon-1]
|
|
# matches original repo's observation_indices=list(range(video_horizon))
|
|
return list(range(self.num_video_frames))
|
|
|
|
@property
|
|
def action_delta_indices(self) -> list[int]:
|
|
return list(range(self.chunk_size))
|
|
|
|
@property
|
|
def reward_delta_indices(self) -> None:
|
|
return None
|