mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-09 11:01:53 +00:00
e275ea3960
* feat(policies): add LingBot-VA autoregressive video-action world model Port the LingBot-VA policy (Wan2.2 dual-stream video+action world model) into LeRobot, following the EO-1 / VLA-JEPA conventions. Covers inference, checkpoint conversion, and predicted-video saving (training is deferred to a follow-up PR). - Vendored Wan transformer/attention/flex/VAE/scheduler modules (key names preserved for near-identity conversion); torch SDPA default, flashattn/flex lazy-guarded. - LingBotVAConfig (registered "lingbot_va") + processor with fixed-quantile action unnormalization; full dual-stream sampling loop with CFG, two flow-matching schedulers and KV cache, mapped onto select_action with observed-keyframe feedback. - convert_lingbot_va_checkpoints.py (libero/robotwin variants): bundles the ~5B transformer, lazy-pulls the frozen VAE+UMT5 from the source repo. - Predicted-video plumbing in lerobot_eval (predicted_frames_callback; opt-in via --policy.save_predicted_video) and ConstantWithWarmupSchedulerConfig. - pyproject: widen diffusers-dep to <0.37, add lingbot_va + imageio-dep extras, add lingbot_va and (missing) eo1 to `all`. - Factory + policies/__init__ wiring, docs page + toctree, and tests. Note: the LIBERO success-rate correctness gate must be validated on a CUDA GPU with the converted checkpoint. * feat(lingbot_va): RoboTwin eef-pose eval, single-file model, Hub checkpoints Make the LingBot-VA port runnable on both LIBERO and RoboTwin and clean up the package to LeRobot conventions. - Consolidate all vendored Wan2.2 model code (transformer, attention, VAE helpers, flow-matching scheduler, grid utils, flex-attention) into a single modeling_lingbot_va.py; remove the separate wan_*/schedulers modules. - Move the fixed action (un)normalization quantiles out of the config and into the post-processor (LIBERO 7-DoF + RoboTwin 16-d eef); remove the conversion script in favour of ready-to-use LeRobot-format checkpoints on the Hub. - Fixes found via on-sim validation: undo LIBERO's 180-degree image flip (image_hflip), encode obs as a multi-frame streaming-VAE clip, reset the streaming VAE cache between episodes, run the transformer in config.dtype, lazy-load frozen VAE/UMT5 by subfolder with the text encoder on CPU. - RoboTwin: add an end-effector-pose action mode to RoboTwinEnv (16-d per-arm xyz+quat+gripper deltas composed onto the initial eef pose, executed via CuRobo IK) and the robotwin_tshape latent layout (full-res head + half-res wrists via a second streaming VAE) with the upstream RoboTwin action quantiles + camera mapping. - Predicted-video saving works for both benchmarks; docs + tests updated. * feat(lingbot_va): implement training / fine-tuning (flow-matching loss) - Implement LingBotVAPolicy.forward(): dual-stream flow-matching training loss (latent + action, timestep-weighted, action-masked) ported from upstream train.py; VAE-encodes camera clips, UMT5-encodes the task, noises both streams, runs the block-causal flex-attention training pass (forward_train). - training_loss_from_streams() core + _build_training_streams() data prep (action scatter into the 30-d space, multi-frame VAE encode incl. robotwin_tshape). - get_optim_params returns only trainable transformer params (LoRA/PEFT friendly); VAE/UMT5 stay frozen. Training needs attn_mode='flex'. - Add a tiny-config single-training-step test (forward->loss->backward->AdamW) and a Training/fine-tuning section in the docs. * fix(lingbot_va): CI quality gate + fast-test collection - Add tests/policies/lingbot_va/__init__.py so the test files don't clash by basename with tests/policies/vla_jepa/* under pytest's default import mode (fast-test collection error). - Fix vendored typos flagged by the typos hook (pach_scale->patch_scale, total_tolen-> total_token_len, stablized->stabilized) and a mypy union-attr in RoboTwinEnv._read_eef_pose. - Apply Prettier formatting to docs/source/lingbot_va.mdx. * docs(lingbot_va): document EEF action-channel schema + camera order * Update lingbot_va.mdx Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com> * Update pyproject.toml Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com> * Update pyproject.toml Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com> * refactor(lingbot_va): drop hardcoded action quantiles; source from checkpoint The LIBERO/RoboTwin action (un)normalization quantiles were hardcoded as module constants in processor_lingbot_va.py. They are already serialized into each checkpoint's policy_postprocessor.json (via LingBotVAActionUnnormalizeStep.get_config) and restored on load by PolicyProcessorPipeline.from_pretrained, so the constants are dead at eval/load time for the released checkpoints (verified: libero_long/robotwin/base all carry their quantiles on the Hub). - Remove LIBERO_ACTION_Q01/Q99, ROBOTWIN_ACTION_Q01/Q99 and _default_action_quantiles. - make_lingbot_va_pre_post_processors now defaults a fresh (unconverted) build to a neutral [-1, 1] mapping (identity rescale); real per-benchmark stats come from the saved checkpoint (or postprocessor_overrides), analogous to dataset-stats normalization. - Update the config doc comment to point at the checkpoint as the source of truth. - Tests: replace the LIBERO-default assertion with a neutral-default check, and add a save_pretrained/from_pretrained round-trip guard for the quantile serialization. * docs(lingbot_va): trim verbose comments - configuration_lingbot_va.py: condense multi-line field comments to one-liners (keep the ── section headers). - processor_lingbot_va.py: shorten the action-quantile explanation block. - modeling_lingbot_va.py: drop the bare "# ----" separator rules, keeping the one-line section headers. No code changes. * docs(lingbot_va): trim provenance comments; default wan path to base repo - configuration_lingbot_va.py: drop the "──" decorations and the "(from transformer/config.json)" note; default wan_pretrained_path to robbyant/lingbot-va-base (has the frozen vae/text_encoder/tokenizer subfolders). - modeling_lingbot_va.py: remove the vendored-code banner and the "(upstream wan_va/...)" section-header provenance/dash decorations; condense the transformer-dtype comment to one line. No code changes. * refactor(lingbot_va): use built-in UnnormalizerProcessorStep for actions Replace the bespoke LingBotVAActionUnnormalizeStep with the standard UnnormalizerProcessorStep in QUANTILES mode, which computes the identical (action + 1) / 2 * (q99 - q01) + q01 mapping. The per-channel q01/q99 are stored as the step's saved state (a safetensors file) and restored on load; a fresh build has no action stats so the step is an identity passthrough. The 3 Hub checkpoints (lerobot/lingbot_va_{libero_long,robotwin,base}) have been re-uploaded with the new post-processor (policy_postprocessor.json + *_unnormalizer_processor.safetensors); reloading from the Hub round-trips q01/q99. - processor_lingbot_va.py: drop the custom step + registry; build the post-processor with UnnormalizerProcessorStep (explicit ACTION->QUANTILES norm_map so the preprocessor / training path is unchanged). - tests: assert the built-in step is used, identity-when-no-stats, correct quantile unnormalization, and a save_pretrained/from_pretrained stats round-trip. * docs(lingbot_va): point checkpoint paths at the lerobot org The LeRobot-format checkpoints moved from pepijn223/* to lerobot/* (libero_long, robotwin, base). Update the eval/train --policy.path examples accordingly. * docs(lingbot_va): condense processor normalization comments * fix(lingbot-va): align RoboTwin evaluation (#3784) Thank you for the RoboTwin fix, and alignment! * applying fixes * updating uv lock and linting * adjusting test to match expected values * cleaning up deps * cleaning up top level imports, styling, and deps guards * cleanup * moving wan utils and loading utils to `utils.py` * removing ftfy by replicating the prompt_clean function without it (we don't expect to have weird chars given in the prompt anyway) * removing unused function * guarding for scipy dep, renaming test to avoid collision * adding back accelerate for peak memory usage optim + justifying robotwin description dep --------- Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com> Co-authored-by: pepijn223 <pepijn223@hf.co> Co-authored-by: Gangwei XU <gwxu@hust.edu.cn> Co-authored-by: Maxime Ellerbach <maxime.ellerbach@huggingface.co>
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
#!/usr/bin/env 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.
|
|
|
|
"""Unit tests for the vendored LingBot-VA helper code (scheduler + grid utilities)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
pytest.importorskip("diffusers") # the model code lives in modeling_lingbot_va, which imports diffusers
|
|
|
|
from lerobot.policies.lingbot_va.modeling_lingbot_va import FlowMatchScheduler
|
|
from lerobot.policies.lingbot_va.utils import data_seq_to_patch, get_mesh_id
|
|
|
|
|
|
def test_flow_match_scheduler_timesteps_monotone_decreasing() -> None:
|
|
sch = FlowMatchScheduler(shift=5.0, sigma_min=0.0, extra_one_step=True)
|
|
sch.set_timesteps(20)
|
|
assert sch.timesteps.shape == (20,)
|
|
diffs = sch.timesteps[1:] - sch.timesteps[:-1]
|
|
assert torch.all(diffs <= 0) # decreasing
|
|
|
|
|
|
def test_flow_match_scheduler_step_preserves_shape() -> None:
|
|
sch = FlowMatchScheduler(shift=5.0, sigma_min=0.0, extra_one_step=True)
|
|
sch.set_timesteps(20)
|
|
sample = torch.zeros(1, 48, 4, 8, 16)
|
|
out = sch.step(torch.ones_like(sample), sch.timesteps[0], sample)
|
|
assert out.shape == sample.shape
|
|
|
|
|
|
def test_flow_match_scheduler_add_noise() -> None:
|
|
sch = FlowMatchScheduler(shift=5.0, sigma_min=0.0, extra_one_step=True)
|
|
sch.set_timesteps(20)
|
|
sample = torch.randn(1, 48, 4, 8, 16)
|
|
noise = torch.randn_like(sample)
|
|
noisy = sch.add_noise(sample, noise, sch.timesteps[:4], t_dim=2)
|
|
assert noisy.shape == sample.shape
|
|
|
|
|
|
def test_get_mesh_id_latent_shape() -> None:
|
|
grid = get_mesh_id(4, 8, 16, 0, 1, 0)
|
|
assert grid.shape == (4, 4 * 8 * 16) # (f, h, w, stream) x tokens
|
|
|
|
|
|
def test_get_mesh_id_action_shape() -> None:
|
|
grid = get_mesh_id(4, 4, 1, 1, 1, 0, action=True)
|
|
assert grid.shape == (4, 4 * 4 * 1)
|
|
# Action rows for h/w are sentinel -1.
|
|
assert torch.all(grid[1] < 0)
|
|
assert torch.all(grid[2] < 0)
|
|
|
|
|
|
def test_data_seq_to_patch_roundtrip_shape() -> None:
|
|
b, f, h, w, c = 1, 4, 8, 16, 48
|
|
seq = torch.arange(b * f * h * w * c, dtype=torch.float32).reshape(b, f * h * w, c)
|
|
out = data_seq_to_patch((1, 2, 2), seq, f, h, w, batch_size=b)
|
|
assert out.shape == (b, c, f, h, w)
|
|
|
|
|
|
def test_training_step_reduces_loss_tiny_flex() -> None:
|
|
"""End-to-end single training step (flow-matching loss -> backward -> AdamW) on a tiny config.
|
|
|
|
Exercises the flex-attention training path; requires a CUDA GPU with flex-attention support.
|
|
"""
|
|
if not torch.cuda.is_available():
|
|
import pytest
|
|
|
|
pytest.skip("training step test requires a CUDA GPU (flex-attention)")
|
|
|
|
from lerobot.configs.types import FeatureType, PolicyFeature
|
|
from lerobot.policies.lingbot_va.configuration_lingbot_va import LingBotVAConfig
|
|
from lerobot.policies.lingbot_va.modeling_lingbot_va import LingBotVAPolicy
|
|
from lerobot.utils.constants import ACTION, OBS_IMAGES
|
|
|
|
cfg = LingBotVAConfig(
|
|
attn_mode="flex",
|
|
dtype="bfloat16",
|
|
in_channels=16,
|
|
out_channels=16,
|
|
action_dim=8,
|
|
text_dim=32,
|
|
freq_dim=64,
|
|
ffn_dim=64,
|
|
num_attention_heads=2,
|
|
attention_head_dim=24,
|
|
num_layers=2,
|
|
frame_chunk_size=2,
|
|
action_per_frame=4,
|
|
used_action_channel_ids=[0, 1, 2, 3],
|
|
obs_cam_keys=[f"{OBS_IMAGES}.image"],
|
|
device="cuda",
|
|
)
|
|
cfg.input_features = {f"{OBS_IMAGES}.image": PolicyFeature(type=FeatureType.VISUAL, shape=(3, 64, 64))}
|
|
cfg.output_features = {ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(4,))}
|
|
cfg.validate_features()
|
|
|
|
policy = LingBotVAPolicy(cfg).to("cuda")
|
|
policy.train()
|
|
opt = torch.optim.AdamW(policy.get_optim_params(), lr=1e-4)
|
|
|
|
b, fc, apf = 1, cfg.frame_chunk_size, cfg.action_per_frame
|
|
latents = torch.randn(b, cfg.in_channels, fc, 4, 4, device="cuda", dtype=torch.bfloat16)
|
|
actions = torch.randn(b, cfg.action_dim, fc, apf, 1, device="cuda", dtype=torch.bfloat16)
|
|
amask = torch.zeros(cfg.action_dim, device="cuda")
|
|
amask[cfg.used_action_channel_ids] = 1.0
|
|
actions_mask = amask.view(1, -1, 1, 1, 1).expand_as(actions)
|
|
text_emb = torch.randn(b, cfg.max_sequence_length, cfg.text_dim, device="cuda", dtype=torch.bfloat16)
|
|
|
|
loss, metrics = policy.training_loss_from_streams(latents, actions, actions_mask, text_emb)
|
|
assert torch.isfinite(loss) and {"latent_loss", "action_loss"} <= set(metrics)
|
|
loss.backward()
|
|
assert any(p.grad is not None and torch.isfinite(p.grad).all() for p in policy.get_optim_params())
|
|
opt.step()
|