mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-22 17:32:07 +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>
172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2024 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.
|
|
import abc
|
|
import logging
|
|
import math
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import draccus
|
|
from torch.optim import Optimizer
|
|
from torch.optim.lr_scheduler import LambdaLR, LRScheduler
|
|
|
|
from lerobot.utils.constants import SCHEDULER_STATE
|
|
from lerobot.utils.import_utils import _diffusers_available, require_package
|
|
from lerobot.utils.io_utils import deserialize_json_into_object, write_json
|
|
|
|
if TYPE_CHECKING or _diffusers_available:
|
|
from diffusers.optimization import get_scheduler
|
|
else:
|
|
get_scheduler = None
|
|
|
|
|
|
@dataclass
|
|
class LRSchedulerConfig(draccus.ChoiceRegistry, abc.ABC):
|
|
num_warmup_steps: int | None
|
|
|
|
@property
|
|
def type(self) -> str:
|
|
return self.get_choice_name(self.__class__)
|
|
|
|
@abc.abstractmethod
|
|
def build(self, optimizer: Optimizer, num_training_steps: int) -> LRScheduler | None:
|
|
raise NotImplementedError
|
|
|
|
|
|
@LRSchedulerConfig.register_subclass("diffuser")
|
|
@dataclass
|
|
class DiffuserSchedulerConfig(LRSchedulerConfig):
|
|
name: str = "cosine"
|
|
num_warmup_steps: int | None = None
|
|
|
|
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
|
|
require_package("diffusers", extra="diffusion")
|
|
|
|
kwargs = {**asdict(self), "num_training_steps": num_training_steps, "optimizer": optimizer}
|
|
return get_scheduler(**kwargs)
|
|
|
|
|
|
@LRSchedulerConfig.register_subclass("vqbet")
|
|
@dataclass
|
|
class VQBeTSchedulerConfig(LRSchedulerConfig):
|
|
num_warmup_steps: int
|
|
num_vqvae_training_steps: int
|
|
num_cycles: float = 0.5
|
|
|
|
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
|
|
def lr_lambda(current_step):
|
|
if current_step < self.num_vqvae_training_steps:
|
|
return float(1)
|
|
else:
|
|
adjusted_step = current_step - self.num_vqvae_training_steps
|
|
if adjusted_step < self.num_warmup_steps:
|
|
return float(adjusted_step) / float(max(1, self.num_warmup_steps))
|
|
progress = float(adjusted_step - self.num_warmup_steps) / float(
|
|
max(1, num_training_steps - self.num_warmup_steps)
|
|
)
|
|
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(self.num_cycles) * 2.0 * progress)))
|
|
|
|
return LambdaLR(optimizer, lr_lambda, -1)
|
|
|
|
|
|
@LRSchedulerConfig.register_subclass("constant_with_warmup")
|
|
@dataclass
|
|
class ConstantWithWarmupSchedulerConfig(LRSchedulerConfig):
|
|
"""Linear warmup followed by a constant learning rate.
|
|
|
|
Mirrors the ``warmup_constant_lambda`` used by LingBot-VA (upstream ``wan_va/train.py``):
|
|
the LR ramps linearly from 0 to the peak over ``num_warmup_steps`` steps, then stays flat.
|
|
"""
|
|
|
|
num_warmup_steps: int = 1000
|
|
|
|
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
|
|
warmup_steps = self.num_warmup_steps or 0
|
|
|
|
def lr_lambda(current_step):
|
|
if current_step < warmup_steps:
|
|
return float(current_step) / float(max(1, warmup_steps))
|
|
return 1.0
|
|
|
|
return LambdaLR(optimizer, lr_lambda, -1)
|
|
|
|
|
|
@LRSchedulerConfig.register_subclass("cosine_decay_with_warmup")
|
|
@dataclass
|
|
class CosineDecayWithWarmupSchedulerConfig(LRSchedulerConfig):
|
|
"""Used by Physical Intelligence to train Pi0.
|
|
|
|
Automatically scales warmup and decay steps if num_training_steps < num_decay_steps.
|
|
This ensures the learning rate schedule completes properly even with shorter training runs.
|
|
"""
|
|
|
|
num_warmup_steps: int
|
|
num_decay_steps: int
|
|
peak_lr: float
|
|
decay_lr: float
|
|
|
|
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
|
|
# Auto-scale scheduler parameters if training steps are shorter than configured decay steps
|
|
actual_warmup_steps = self.num_warmup_steps
|
|
actual_decay_steps = self.num_decay_steps
|
|
|
|
if num_training_steps < self.num_decay_steps:
|
|
# Calculate scaling factor to fit the schedule into the available training steps
|
|
scale_factor = num_training_steps / self.num_decay_steps
|
|
actual_warmup_steps = int(self.num_warmup_steps * scale_factor)
|
|
actual_decay_steps = num_training_steps
|
|
|
|
logging.info(
|
|
f"Auto-scaling LR scheduler: "
|
|
f"num_training_steps ({num_training_steps}) < num_decay_steps ({self.num_decay_steps}). "
|
|
f"Scaling warmup: {self.num_warmup_steps} → {actual_warmup_steps}, "
|
|
f"decay: {self.num_decay_steps} → {actual_decay_steps} "
|
|
f"(scale factor: {scale_factor:.3f})"
|
|
)
|
|
|
|
def lr_lambda(current_step):
|
|
def linear_warmup_schedule(current_step):
|
|
if current_step <= 0:
|
|
return 1 / (actual_warmup_steps + 1)
|
|
frac = 1 - current_step / actual_warmup_steps
|
|
return (1 / (actual_warmup_steps + 1) - 1) * frac + 1
|
|
|
|
def cosine_decay_schedule(current_step):
|
|
step = min(current_step, actual_decay_steps)
|
|
cosine_decay = 0.5 * (1 + math.cos(math.pi * step / actual_decay_steps))
|
|
alpha = self.decay_lr / self.peak_lr
|
|
decayed = (1 - alpha) * cosine_decay + alpha
|
|
return decayed
|
|
|
|
if current_step < actual_warmup_steps:
|
|
return linear_warmup_schedule(current_step)
|
|
|
|
return cosine_decay_schedule(current_step)
|
|
|
|
return LambdaLR(optimizer, lr_lambda, -1)
|
|
|
|
|
|
def save_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> None:
|
|
state_dict = scheduler.state_dict()
|
|
write_json(state_dict, save_dir / SCHEDULER_STATE)
|
|
|
|
|
|
def load_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> LRScheduler:
|
|
state_dict = deserialize_json_into_object(save_dir / SCHEDULER_STATE, scheduler.state_dict())
|
|
scheduler.load_state_dict(state_dict)
|
|
return scheduler
|