mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-17 23:11:45 +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>
158 lines
5.6 KiB
Python
158 lines
5.6 KiB
Python
#!/usr/bin/env python
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
pytest.importorskip("diffusers")
|
|
|
|
from conftest import (
|
|
ACTION_DIM,
|
|
ACTION_HORIZON,
|
|
BATCH_SIZE,
|
|
QWEN_HIDDEN_SIZE,
|
|
STATE_DIM,
|
|
make_config,
|
|
set_seed_all,
|
|
) # noqa: E402
|
|
|
|
from lerobot.policies.vla_jepa.action_head import ( # noqa: E402
|
|
VLAJEPAActionHead,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VLAJEPAActionHead
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action_dim,state_dim,action_horizon",
|
|
[
|
|
(3, 4, 4), # default test dims
|
|
(7, 0, 16), # no proprioceptive state, production-like action space
|
|
(6, 8, 8), # medium dims
|
|
],
|
|
)
|
|
def test_action_head_sample_time_range(action_dim: int, state_dim: int, action_horizon: int) -> None:
|
|
config = make_config(action_dim=action_dim, state_dim=state_dim, action_horizon=action_horizon)
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
t = head.sample_time(batch_size=200, device=torch.device("cpu"), dtype=torch.float32)
|
|
assert t.shape == (200,)
|
|
assert torch.isfinite(t).all()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action_dim,state_dim,action_horizon",
|
|
[
|
|
(3, 4, 4),
|
|
(7, 0, 16),
|
|
(6, 8, 8),
|
|
],
|
|
)
|
|
def test_action_head_build_inputs_shape(action_dim: int, state_dim: int, action_horizon: int) -> None:
|
|
config = make_config(action_dim=action_dim, state_dim=state_dim, action_horizon=action_horizon)
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(2, 4, QWEN_HIDDEN_SIZE)
|
|
actions = torch.randn(2, action_horizon, action_dim)
|
|
timesteps = torch.randint(0, 100, (2,))
|
|
|
|
state = torch.randn(2, state_dim) if state_dim > 0 else None
|
|
out_with = head._build_inputs(conditioning, actions, state, timesteps)
|
|
out_none = head._build_inputs(conditioning, actions, None, timesteps)
|
|
|
|
assert out_with.ndim == 3 and out_none.ndim == 3
|
|
if state_dim > 0:
|
|
assert out_with.shape[1] > out_none.shape[1]
|
|
assert torch.isfinite(out_with).all() and torch.isfinite(out_none).all()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action_dim,state_dim,action_horizon",
|
|
[
|
|
(3, 4, 4),
|
|
(7, 0, 16),
|
|
(6, 8, 8),
|
|
],
|
|
)
|
|
def test_action_head_forward_loss_valid(action_dim: int, state_dim: int, action_horizon: int) -> None:
|
|
set_seed_all(42)
|
|
config = make_config(action_dim=action_dim, state_dim=state_dim, action_horizon=action_horizon)
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(2, 4, QWEN_HIDDEN_SIZE)
|
|
actions = torch.randn(2, action_horizon, action_dim)
|
|
state = torch.randn(2, state_dim) if state_dim > 0 else None
|
|
loss = head.forward(conditioning, actions, state)
|
|
assert loss.shape == ()
|
|
assert torch.isfinite(loss) and loss > 0
|
|
|
|
|
|
def test_action_head_forward_gradient_flows() -> None:
|
|
set_seed_all(42)
|
|
config = make_config()
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(BATCH_SIZE, 4, QWEN_HIDDEN_SIZE)
|
|
actions = torch.randn(BATCH_SIZE, ACTION_HORIZON, ACTION_DIM)
|
|
state = torch.randn(BATCH_SIZE, STATE_DIM)
|
|
loss = head.forward(conditioning, actions, state)
|
|
loss.backward()
|
|
assert any(p.grad is not None for p in head.parameters() if p.requires_grad)
|
|
|
|
|
|
@torch.no_grad()
|
|
@pytest.mark.parametrize(
|
|
"action_dim,state_dim,action_horizon",
|
|
[
|
|
(3, 4, 4),
|
|
(7, 0, 16),
|
|
(6, 8, 8),
|
|
],
|
|
)
|
|
def test_action_head_predict_action_shape(action_dim: int, state_dim: int, action_horizon: int) -> None:
|
|
set_seed_all(42)
|
|
config = make_config(action_dim=action_dim, state_dim=state_dim, action_horizon=action_horizon)
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(2, 4, QWEN_HIDDEN_SIZE)
|
|
state = torch.randn(2, state_dim) if state_dim > 0 else None
|
|
pred = head.predict_action(conditioning, state)
|
|
assert tuple(pred.shape) == (2, action_horizon, action_dim)
|
|
assert torch.isfinite(pred).all()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# action_is_pad masking
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_action_head_loss_fully_padded_is_zero() -> None:
|
|
"""Loss is 0 when every timestep is padded (exercises the clamp_min guard)."""
|
|
set_seed_all(42)
|
|
config = make_config()
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(BATCH_SIZE, 4, QWEN_HIDDEN_SIZE)
|
|
actions = torch.randn(BATCH_SIZE, ACTION_HORIZON, ACTION_DIM)
|
|
state = torch.randn(BATCH_SIZE, STATE_DIM)
|
|
|
|
action_is_pad = torch.ones(BATCH_SIZE, ACTION_HORIZON, dtype=torch.bool)
|
|
loss = head.forward(conditioning, actions, state, action_is_pad)
|
|
assert loss.item() == 0.0
|
|
|
|
|
|
def test_action_head_loss_none_matches_no_padding() -> None:
|
|
"""action_is_pad=None is equivalent to an all-False (no padding) mask."""
|
|
set_seed_all(42)
|
|
config = make_config()
|
|
head = VLAJEPAActionHead(config, cross_attention_dim=QWEN_HIDDEN_SIZE)
|
|
conditioning = torch.randn(BATCH_SIZE, 4, QWEN_HIDDEN_SIZE)
|
|
actions = torch.randn(BATCH_SIZE, ACTION_HORIZON, ACTION_DIM)
|
|
state = torch.randn(BATCH_SIZE, STATE_DIM)
|
|
|
|
set_seed_all(0)
|
|
loss_none = head.forward(conditioning, actions, state, action_is_pad=None)
|
|
|
|
set_seed_all(0)
|
|
no_pad = torch.zeros(BATCH_SIZE, ACTION_HORIZON, dtype=torch.bool)
|
|
loss_zeros = head.forward(conditioning, actions, state, action_is_pad=no_pad)
|
|
|
|
assert torch.isclose(loss_none, loss_zeros)
|