diff --git a/README.md b/README.md index f72952102..acfad05fb 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ lerobot-train \ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Imitation Learning** | [ACT](./docs/source/policy_act_README.md), [Diffusion](./docs/source/policy_diffusion_README.md), [VQ-BeT](./docs/source/policy_vqbet_README.md), [Multitask DiT Policy](./docs/source/policy_multi_task_dit_README.md) | | **Reinforcement Learning** | [HIL-SERL](./docs/source/hilserl.mdx), [TDMPC](./docs/source/policy_tdmpc_README.md) & QC-FQL (coming soon) | -| **VLAs Models** | [Pi0](./docs/source/pi0.mdx), [Pi0Fast](./docs/source/pi0fast.mdx), [Pi0.5](./docs/source/pi05.mdx), [GR00T N1.5](./docs/source/policy_groot_README.md), [SmolVLA](./docs/source/policy_smolvla_README.md), [XVLA](./docs/source/xvla.mdx), [EO-1](./docs/source/eo1.mdx), [MolmoAct2](./docs/source/molmoact2.mdx), [WALL-OSS](./docs/source/walloss.mdx) | +| **VLAs Models** | [Pi0](./docs/source/pi0.mdx), [Pi0Fast](./docs/source/pi0fast.mdx), [Pi0.5](./docs/source/pi05.mdx), [GR00T N1.7](./docs/source/policy_groot_README.md), [SmolVLA](./docs/source/policy_smolvla_README.md), [XVLA](./docs/source/xvla.mdx), [EO-1](./docs/source/eo1.mdx), [MolmoAct2](./docs/source/molmoact2.mdx), [WALL-OSS](./docs/source/walloss.mdx) | | **World Models** | [VLA-JEPA](./docs/source/vla_jepa.mdx) (more coming soon) | | **Reward Models** | [SARM](./docs/source/sarm.mdx), [TOPReward](./docs/source/topreward.mdx), [Robometer](./docs/source/robometer.mdx) | diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml index 79f4bc124..d4bdf892e 100644 --- a/docs/source/_toctree.yml +++ b/docs/source/_toctree.yml @@ -74,7 +74,7 @@ - local: fastwam title: FastWAM - local: groot - title: NVIDIA GR00T N1.5 + title: NVIDIA GR00T - local: xvla title: X-VLA - local: multi_task_dit diff --git a/docs/source/envhub_isaaclab_arena.mdx b/docs/source/envhub_isaaclab_arena.mdx index b934240d6..cd077806d 100644 --- a/docs/source/envhub_isaaclab_arena.mdx +++ b/docs/source/envhub_isaaclab_arena.mdx @@ -193,7 +193,7 @@ To learn more about training policies with LeRobot, please refer to the training - [SmolVLA](./smolvla) - [Pi0.5](./pi05) -- [GR00T N1.5](./groot) +- [GR00T N1.7](./groot) Sample IsaacLab Arena datasets are available on HuggingFace Hub for experimentation: diff --git a/docs/source/groot.mdx b/docs/source/groot.mdx index 3ab202fb2..af6bfe1ab 100644 --- a/docs/source/groot.mdx +++ b/docs/source/groot.mdx @@ -1,16 +1,19 @@ -# GR00T N1.5 Policy +# GR00T Policy -GR00T N1.5 is an open foundation model from NVIDIA designed for generalized humanoid robot reasoning and skills. It is a cross-embodiment model that accepts multimodal input, including language and images, to perform manipulation tasks in diverse environments. +GR00T is an NVIDIA foundation model family for generalized humanoid robot reasoning and skills. It is a cross-embodiment policy that accepts multimodal input, including language, images, and proprioception, to perform manipulation tasks in diverse environments. -This document outlines the specifics of its integration and usage within the LeRobot framework. +LeRobot integrates GR00T N1.7 through the `groot` policy type. + +> [!WARNING] +> **Breaking change:** GR00T N1.5 support was removed from LeRobot, and current releases support GR00T N1.7 only. N1.5 checkpoints and configs are rejected with a migration note. To keep using an N1.5 checkpoint, pin the last release that supports it: `pip install 'lerobot==0.5.1'`. To use the current release, migrate to GR00T N1.7 (base model [`nvidia/GR00T-N1.7-3B`](https://huggingface.co/nvidia/GR00T-N1.7-3B)). ## Model Overview -NVIDIA Isaac GR00T N1.5 is an upgraded version of the GR00T N1 foundation model. It is built to improve generalization and language-following abilities for humanoid robots. +GR00T N1.7 uses a Cosmos-Reason2/Qwen3-VL backbone and provides checkpoints for SimplerEnv, DROID, and LIBERO. -Developers and researchers can post-train GR00T N1.5 with their own real or synthetic data to adapt it for specific humanoid robots or tasks. +Developers and researchers can post-train GR00T with their own real or synthetic data to adapt it for specific humanoid robots or tasks. -GR00T N1.5 (specifically the GR00T-N1.5-3B model) is built using pre-trained vision and language encoders. It utilizes a flow matching action transformer to model a chunk of actions, conditioned on vision, language, and proprioception. +GR00T uses pre-trained vision and language encoders with a flow matching action transformer to model a chunk of actions conditioned on vision, language, and proprioception. =2.2.1,<2.8.0" "torchvision>=0.21.0,<0.23.0" # --index-url https://download.pytorch.org/whl/cu1XX -pip install ninja "packaging>=24.2,<26.0" # flash attention dependencies -pip install "flash-attn>=2.5.9,<3.0.0" --no-build-isolation -python -c "import flash_attn; print(f'Flash Attention {flash_attn.__version__} imported successfully')" +pip install "lerobot[groot]" ``` -3. Install LeRobot by running: +For a source checkout: ```bash -pip install lerobot[groot] +pip install -e ".[groot]" ``` ## Usage -To use GR00T in your LeRobot configuration, specify the policy type as: +To use GR00T N1.7: -```python -policy.type=groot +```bash +--policy.type=groot ``` ## Training @@ -63,72 +57,171 @@ policy.type=groot Here's a complete training command for finetuning the base GR00T model on your own dataset: +This command is using the `new_embodiment` flag, which is used for the SO-101 robot, [read more about how GR00T handles different embodiments.](https://github.com/NVIDIA/Isaac-GR00T/blob/main/getting_started/policy.md#--embodiment-tag). + ```bash -# Using a multi-GPU setup -accelerate launch \ - --multi_gpu \ - --num_processes=$NUM_GPUS \ - $(which lerobot-train) \ - --output_dir=$OUTPUT_DIR \ - --save_checkpoint=true \ - --batch_size=$BATCH_SIZE \ - --steps=$NUM_STEPS \ - --save_freq=$SAVE_FREQ \ - --log_freq=$LOG_FREQ \ - --policy.push_to_hub=true \ +# install extra deps for training +pip install "lerobot[training]" + +hf auth login +wandb login + +export DATASET_NAME=your_data_set +export HF_USER=your_hf_username +export DATASET=$HF_USER/$DATASET_NAME +export REPO_ID="${DATASET}_GR00T17" #this is the model that will be uploaded to huggingface +export OUTPUT_DIR=outputs/train/$REPO_ID + +lerobot-train \ + --dataset.repo_id=$DATASET \ + --dataset.image_transforms.enable=true \ --policy.type=groot \ + --policy.device=cuda \ + --policy.base_model_path=nvidia/GR00T-N1.7-3B \ + --policy.embodiment_tag=new_embodiment \ + --policy.chunk_size=16 \ + --policy.n_action_steps=16 \ + --policy.use_relative_actions=true \ + --policy.relative_exclude_joints='["gripper"]' \ + --policy.use_bf16=true \ + --policy.push_to_hub=true \ --policy.repo_id=$REPO_ID \ - --policy.tune_diffusion_model=false \ - --dataset.repo_id=$DATASET_ID \ + --seed=42 \ + --batch_size=64 \ + --steps=20000 \ + --save_checkpoint=true \ + --save_freq=5000 \ + --use_policy_training_preset=true \ + --env_eval_freq=0 \ + --eval_steps=0 \ + --log_freq=10 \ + --output_dir=$OUTPUT_DIR \ + --job_name=$DATASET \ --wandb.enable=true \ - --wandb.disable_artifact=true \ - --job_name=$JOB_NAME + --wandb.disable_artifact=true + ``` ## Performance Results -### Libero Benchmark Results +### LIBERO Benchmark Results > [!NOTE] -> Follow our instructions for Libero usage: [Libero](./libero) +> Follow the [LIBERO](./libero) setup instructions before running `lerobot-eval`. -GR00T has demonstrated strong performance on the Libero benchmark suite. To compare and test its LeRobot implementation, we finetuned the GR00T N1.5 model for 30k steps on the Libero dataset and compared the results to the GR00T reference results. +GR00T N1.7 has demonstrated strong performance on the LIBERO benchmark suite. To reproduce LeRobot results, follow the instructions in the [LIBERO](./libero) section. -| Benchmark | LeRobot Implementation | GR00T Reference | -| ------------------ | ---------------------- | --------------- | -| **Libero Spatial** | 82.0% | 92.0% | -| **Libero Object** | 99.0% | 92.0% | -| **Libero Long** | 82.0% | 76.0% | -| **Average** | 87.0% | 87.0% | +### Train on LIBERO -These results demonstrate GR00T's strong generalization capabilities across diverse robotic manipulation tasks. To reproduce these results, you can follow the instructions in the [Libero](https://huggingface.co/docs/lerobot/libero) section. +Example training command for a LIBERO suite (here `libero_spatial`): + +```bash +IMAGE_TRANSFORMS='{ + "brightness": {"weight": 1.0, "type": "ColorJitter", "kwargs": {"brightness": [0.7, 1.3]}}, + "contrast": {"weight": 1.0, "type": "ColorJitter", "kwargs": {"contrast": [0.6, 1.4]}}, + "saturation": {"weight": 1.0, "type": "ColorJitter", "kwargs": {"saturation": [0.5, 1.5]}}, + "hue": {"weight": 1.0, "type": "ColorJitter", "kwargs": {"hue": [-0.08, 0.08]}} +}' + +lerobot-train \ + --dataset.repo_id=IPEC-COMMUNITY/libero_spatial_no_noops_1.0.0_lerobot \ + --dataset.root=/datasets/libero_spatial \ + --dataset.revision=main \ + --dataset.video_backend=pyav \ + --dataset.image_transforms.enable=true \ + --dataset.image_transforms.max_num_transforms=4 \ + --dataset.image_transforms.tfs="$IMAGE_TRANSFORMS" \ + --policy.type=groot \ + --policy.base_model_path=nvidia/GR00T-N1.7-3B \ + --policy.embodiment_tag=libero_sim \ + --policy.push_to_hub=false \ + --policy.use_relative_actions=false \ + --policy.max_steps=20000 \ + --batch_size=320 \ + --steps=20000 \ + --save_freq=2000 \ + --env_eval_freq=0 \ + --eval_steps=0 \ + --log_freq=10 \ + --wandb.enable=true \ + --wandb.project=lerobot \ + --wandb.mode=online \ + --wandb.disable_artifact=true \ + --num_workers=4 \ + --prefetch_factor=2 \ + --persistent_workers=true \ + --output_dir=$OUTPUT_DIR \ + --job_name=$JOB_NAME +``` + +This will follow the recipe found [here](https://github.com/NVIDIA/Isaac-GR00T/blob/main/examples/LIBERO/README.md). + +### GR00T N1.7 LIBERO Results + +Preliminary LeRobot integration results (GR00T-LeRobot, `eval.n_episodes >= 50` per suite): + +| Suite | Success rate | +| ---------------- | -----------: | +| LIBERO Spatial | 94% | +| LIBERO Object | 98% | +| LIBERO Goal | 93% | +| LIBERO 10 (Long) | 90% | +| **Average** | **93.75%** | + +```bash +export MODEL_ID=your_trained_model_on_huggingface + +lerobot-eval \ + --policy.type=groot \ + --policy.base_model_path=$MODEL_ID \ + --policy.embodiment_tag=libero_sim \ + --env.type=libero \ + --env.task=libero_spatial \ + --eval.n_episodes=50 +``` + +Use `eval.n_episodes >= 50` per suite when reporting success rates. ### Evaluate in your hardware setup Once you have trained your model using your parameters you can run inference in your downstream task. Follow the instructions in [Policy Deployment (lerobot-rollout)](./inference). For example: ```bash -lerobot-rollout\ - --strategy.type=sentry \ - --strategy.upload_every_n_episodes=5 \ - --robot.type=bi_so_follower \ - --robot.left_arm_port=/dev/ttyACM1 \ - --robot.right_arm_port=/dev/ttyACM0 \ - --robot.id=bimanual_follower \ - --robot.cameras='{ right: {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}, - left: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}, - top: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}, - }' \ +# install extra deps for roullout and real hardware +pip install "lerobot[feetech,viz]" + +export MODEL_ID=your_trained_model_on_huggingface + +# make sure that camera index matches your setup! +# find index using `uv run lerobot-find-cameras opencv` +WRIST_CAM='wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30, fourcc: "MJPG"}' +FRONT_CAM='front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30, fourcc: "MJPG"}' +export ROBOT_CAMERAS="{ $WRIST_CAM, $FRONT_CAM }" +export ROBOT_ID=follower_robot +export ROBOT_PORT=/dev/ttyACM0 + +uv run lerobot-rollout \ + --strategy.type=base \ + --policy.path=$MODEL_ID \ + --policy.base_model_path=nvidia/GR00T-N1.7-3B \ + --policy.n_action_steps=8 \ + --robot.type=so101_follower \ + --robot.port=$ROBOT_PORT \ + --robot.id=$ROBOT_ID \ + --robot.cameras="$ROBOT_CAMERAS" \ + --task="place the vial in the rack" \ + --duration=60 \ + --device=cuda \ --display_data=true \ - --dataset.repo_id=/eval_groot-bimanual \ - --dataset.single_task="Grab and handover the red cube to the other arm" \ - --dataset.streaming_encoding=true \ - --dataset.encoder_threads=2 \ - # --dataset.rgb_encoder.vcodec=auto \ - --policy.path=/groot-bimanual \ # your trained model - --duration=600 + --inference.type=rtc \ + --inference.rtc.enabled=True \ # set to False if it causes inference instability + --inference.rtc.execution_horizon=8 \ + --inference.queue_threshold=0 ``` +> [!NOTE] +> Value of `inference.queue_threshold` should not exceed 5 to ensure stable inference. + ## License -This model follows NVIDIA's proprietary license, consistent with the original [GR00T repository](https://github.com/NVIDIA/Isaac-GR00T). Future versions (starting from N1.7) will follow **Apache 2.0 License**. +GR00T N1.7 is released under the [NVIDIA Open Model License Agreement](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/). diff --git a/docs/source/policy_groot_README.md b/docs/source/policy_groot_README.md index efcd76ebe..4b256fb27 100644 --- a/docs/source/policy_groot_README.md +++ b/docs/source/policy_groot_README.md @@ -1,6 +1,13 @@ ## Research Paper -Paper: https://research.nvidia.com/labs/gear/gr00t-n1_5/ +GR00T N1 technical report (covers the GR00T N1.x family, including N1.7): https://arxiv.org/abs/2503.14734 + +GR00T N1.7 model card: https://huggingface.co/nvidia/GR00T-N1.7-3B + +GR00T N1.5 research page (earlier version): https://research.nvidia.com/labs/gear/gr00t-n1_5/ + +> GR00T N1.5 support was removed from LeRobot; the last release supporting it is `lerobot==0.5.1`. +> Current releases support GR00T N1.7 only. ## Repository @@ -24,4 +31,108 @@ Code: https://github.com/NVIDIA/Isaac-GR00T Blog: https://developer.nvidia.com/isaac/gr00t -Hugging Face Model: https://huggingface.co/nvidia/GR00T-N1.5-3B +Hugging Face Models: + +- GR00T N1.7: https://huggingface.co/nvidia/GR00T-N1.7-3B +- GR00T N1.7 LIBERO checkpoints: https://huggingface.co/nvidia/GR00T-N1.7-LIBERO + +
+Original-vs-LeRobot parity test + +## Original-vs-LeRobot parity test + +`tests/policies/groot/test_groot_vs_original.py` verifies this LeRobot +reimplementation of GR00T N1.7 (Qwen3-VL backbone + flow-matching action head) +against NVIDIA's original `gr00t` package with two comparisons, each parametrized +over every embodiment tag present in the checkpoint: + +1. **Model parity** — given byte-identical pre-processed inputs and the same + flow-matching seed (recorded in each artifact), both implementations must produce + the **same raw model output** (`get_action(...)["action_pred"]`, the normalized + flow-matching prediction). Output shapes must match exactly; any action-horizon + or action-dim mismatch fails the test. +2. **Preprocessor parity** — given the identical raw observations (per-camera + frames, state vectors, language instruction), LeRobot's own preprocessor pipeline + (real Qwen3-VL chat template / tokenizer / image packing + checkpoint-driven + state normalization, no mocks) must produce the **same collated model inputs** + (`input_ids`, `attention_mask`, `pixel_values`, `image_grid_thw`, `state`, + `embodiment_id`) as the original package's processor. + +### Why two environments + +The original `gr00t` package pins `transformers==4.57.3` (Python 3.10); this +integration requires `transformers>=5.x` (Qwen3-VL). Under 5.x, `PretrainedConfig` +is itself a defaulted dataclass, so the original config dataclasses fail to import +(`non-default argument follows default argument`). The two implementations therefore +**cannot be imported in the same Python process**. + +So the test uses a **producer / consumer** split across two venvs: + +1. **Producer** — `tests/policies/groot/utils/dump_original_n1_7.py`, run in the _original_ + gr00t venv. For each embodiment it builds dummy inputs generically from the + checkpoint metadata (state dims from `statistics.json`; camera/language keys from + the processor modality configs), runs the original model, and saves to one `.npz` + per tag: the raw observations (`raw::` keys), the exact collated inputs + (`in::` keys), the seed, and the raw `action_pred`. +2. **Consumer** — the pytest above, run in the _LeRobot_ venv. It discovers every + `.npz`; the model-parity case replays the byte-identical collated inputs through + the LeRobot model with the recorded seed and asserts the outputs match, and the + preprocessor-parity case replays the raw observations through LeRobot's full + preprocessor pipeline and asserts the collated tensors match. + +> Artifacts generated by older versions of the dump script contain no `raw::` +> fields; the preprocessor-parity case then **skips** with a regeneration hint. +> Re-run the producer to refresh them. + +### Fairness controls + +- **Same pre-processed inputs (model parity)** — the original processor's `input_ids`, + `pixel_values`, `image_grid_thw`, `attention_mask`, `state`, `embodiment_id` are + fed verbatim to the LeRobot model (no re-tokenization / re-normalization), so the + model comparison isolates the model. LeRobot's own tokenization / image packing is + covered separately by the preprocessor-parity case, which compares its output + against those same collated tensors from identical raw observations. +- **Same precision + attention kernel** — both sides run **fp32 + SDPA**. The + original defaults to `use_flash_attention=True` (flash_attention_2 + bf16); the + producer forces SDPA + fp32. (With the defaults the gap is ~3e-2 — pure + kernel/rounding noise, not an implementation difference.) +- **Same flow-matching seed** — fixed right before sampling on both sides; the + producer records it in each artifact (`--seed`, default 42) and the consumer + replays the recorded value. + +### How to run + +```bash +# Resolve a local checkpoint (GR00T-N1.7-LIBERO / libero_10) +CKPT=$(python - <<'PY' +import os +from huggingface_hub import snapshot_download +print(os.path.join(snapshot_download("nvidia/GR00T-N1.7-LIBERO", + allow_patterns=["libero_10/*"]), "libero_10")) +PY +) + +# 1) Produce the original-side artifacts for all embodiments (original gr00t venv, CUDA) +CUDA_VISIBLE_DEVICES=0 /path/to/Isaac-GR00T/.venv-original/bin/python \ + tests/policies/groot/utils/dump_original_n1_7.py \ + --ckpt "$CKPT" --out-dir tests/policies/groot/artifacts --device cuda --seed 42 + +# 2) Run the parity test (LeRobot venv) — one parametrized case per embodiment +CUDA_VISIBLE_DEVICES=0 GROOT_PARITY_DEVICE=cuda \ + uv run pytest tests/policies/groot/test_groot_vs_original.py -v -s +``` + +The `.npz` artifacts are local-only (gitignored, ~6–10 MB each) and are regenerated by +the producer; they are never committed. The tests **skip** (do not fail) on CI or +when the checkpoint / artifacts are absent. + +#### Env knobs (all optional) + +| Var | Default | Purpose | +| ----------------------------------------- | -------------------------------- | ------------------------------------- | +| `GROOT_N1_7_PARITY_DIR` | `tests/policies/groot/artifacts` | directory of per-tag `.npz` artifacts | +| `GROOT_N1_7_LIBERO_CKPT` | auto (HF cache) | override checkpoint dir | +| `GROOT_PARITY_DEVICE` | `cuda` if available | `cpu` or `cuda` | +| `GROOT_PARITY_ATOL` / `GROOT_PARITY_RTOL` | `1e-3` | comparison tolerance | + +
diff --git a/pyproject.toml b/pyproject.toml index 84fe07328..882dd0b6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -219,11 +219,10 @@ groot = [ "lerobot[transformers-dep]", "lerobot[peft-dep]", "lerobot[diffusers-dep]", + "lerobot[dataset]", # NOTE: processor_groot builds a LeRobotDataset for relative-action training stats "dm-tree>=0.1.8,<1.0.0", "timm>=1.0.0,<1.1.0", "decord>=0.6.0,<1.0.0; (platform_machine == 'AMD64' or platform_machine == 'x86_64')", - "ninja>=1.11.1,<2.0.0", - "flash-attn>=2.5.9,<3.0.0 ; sys_platform != 'darwin'" ] sarm = ["lerobot[transformers-dep]", "pydantic>=2.0.0,<3.0.0", "faker>=33.0.0,<35.0.0", "lerobot[matplotlib-dep]", "lerobot[qwen-vl-utils-dep]"] robometer = ["lerobot[transformers-dep]", "lerobot[qwen-vl-utils-dep]", "lerobot[peft-dep]"] @@ -315,7 +314,7 @@ all = [ "lerobot[molmoact2]", "lerobot[smolvla]", "lerobot[fastwam]", - # "lerobot[groot]", TODO(Steven): Gr00t requires specific installation instructions for flash-attn + "lerobot[groot]", "lerobot[xvla]", "lerobot[hilserl]", "lerobot[vla_jepa]", diff --git a/src/lerobot/policies/factory.py b/src/lerobot/policies/factory.py index 12871218e..483804a57 100644 --- a/src/lerobot/policies/factory.py +++ b/src/lerobot/policies/factory.py @@ -295,26 +295,23 @@ def make_pre_post_processors( policy configuration type. """ if pretrained_path: - # TODO(Steven): Temporary patch, implement correctly the processors for Gr00t if isinstance(policy_cfg, GrootConfig): - # GROOT handles normalization in groot_pack_inputs_v3 step - # Need to override both stats AND normalize_min_max since saved config might be empty - preprocessor_overrides = {} - postprocessor_overrides = {} - preprocessor_overrides["groot_pack_inputs_v3"] = { - "stats": kwargs.get("dataset_stats"), - "normalize_min_max": True, - } + from .groot.processor_groot import make_groot_pre_post_processors_from_pretrained - # Also ensure postprocessing slices to env action dim and unnormalizes with dataset stats - env_action_dim = policy_cfg.output_features[ACTION].shape[0] - postprocessor_overrides["groot_action_unpack_unnormalize_v1"] = { - "stats": kwargs.get("dataset_stats"), - "normalize_min_max": True, - "env_action_dim": env_action_dim, - } - kwargs["preprocessor_overrides"] = preprocessor_overrides - kwargs["postprocessor_overrides"] = postprocessor_overrides + return make_groot_pre_post_processors_from_pretrained( + config=policy_cfg, + pretrained_path=pretrained_path, + dataset_stats=kwargs.get("dataset_stats"), + dataset_meta=kwargs.get("dataset_meta"), + preprocessor_overrides=kwargs.get("preprocessor_overrides"), + postprocessor_overrides=kwargs.get("postprocessor_overrides"), + preprocessor_config_filename=kwargs.get( + "preprocessor_config_filename", f"{POLICY_PREPROCESSOR_DEFAULT_NAME}.json" + ), + postprocessor_config_filename=kwargs.get( + "postprocessor_config_filename", f"{POLICY_POSTPROCESSOR_DEFAULT_NAME}.json" + ), + ) preprocessor = PolicyProcessorPipeline.from_pretrained( pretrained_model_name_or_path=pretrained_path, @@ -420,6 +417,7 @@ def make_pre_post_processors( processors = make_groot_pre_post_processors( config=policy_cfg, dataset_stats=kwargs.get("dataset_stats"), + dataset_meta=kwargs.get("dataset_meta"), ) elif isinstance(policy_cfg, XVLAConfig): @@ -570,6 +568,7 @@ def make_policy( set_dataset_feature_metadata = getattr(cfg, "set_dataset_feature_metadata", None) if callable(set_dataset_feature_metadata): set_dataset_feature_metadata(ds_meta.features) + cfg._runtime_dataset_meta = ds_meta kwargs["config"] = cfg diff --git a/src/lerobot/policies/groot/action_head/action_encoder.py b/src/lerobot/policies/groot/action_head/action_encoder.py deleted file mode 100644 index c6fa0a779..000000000 --- a/src/lerobot/policies/groot/action_head/action_encoder.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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 torch -import torch.nn as nn - - -def swish(x): - return x * torch.sigmoid(x) - - -class SinusoidalPositionalEncoding(nn.Module): - """ - Produces a sinusoidal encoding of shape (B, T, w) - given timesteps of shape (B, T). - """ - - def __init__(self, embedding_dim): - super().__init__() - self.embedding_dim = embedding_dim - - def forward(self, timesteps): - # timesteps: shape (B, T) - # We'll compute sin/cos frequencies across dim T - timesteps = timesteps.float() # ensure float - - b, t = timesteps.shape - device = timesteps.device - - half_dim = self.embedding_dim // 2 - # typical log space frequencies for sinusoidal encoding - exponent = -torch.arange(half_dim, dtype=torch.float, device=device) * ( - torch.log(torch.tensor(10000.0)) / half_dim - ) - # Expand timesteps to (B, T, 1) then multiply - freqs = timesteps.unsqueeze(-1) * exponent.exp() # (B, T, half_dim) - - sin = torch.sin(freqs) - cos = torch.cos(freqs) - enc = torch.cat([sin, cos], dim=-1) # (B, T, w) - - return enc diff --git a/src/lerobot/policies/groot/action_head/cross_attention_dit.py b/src/lerobot/policies/groot/action_head/cross_attention_dit.py index a4cd1a0b7..697459240 100755 --- a/src/lerobot/policies/groot/action_head/cross_attention_dit.py +++ b/src/lerobot/policies/groot/action_head/cross_attention_dit.py @@ -1,11 +1,12 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 +#!/usr/bin/env python + +# Copyright 2025 NVIDIA Corporation and 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 +# 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, @@ -14,6 +15,7 @@ # limitations under the License. +import logging from typing import TYPE_CHECKING import torch @@ -42,6 +44,9 @@ else: Timesteps = None +logger = logging.getLogger(__name__) + + class TimestepEncoder(nn.Module): def __init__(self, embedding_dim, compute_dtype=torch.float32): require_package("diffusers", extra="groot") @@ -181,8 +186,7 @@ class BasicTransformerBlock(nn.Module): attn_output = self.attn1( norm_hidden_states, encoder_hidden_states=encoder_hidden_states, - attention_mask=attention_mask, - # encoder_attention_mask=encoder_attention_mask, + attention_mask=encoder_attention_mask if encoder_hidden_states is not None else attention_mask, ) if self.final_dropout: attn_output = self.final_dropout(attn_output) @@ -266,8 +270,8 @@ class DiT(ModelMixin, ConfigMixin): self.norm_out = nn.LayerNorm(self.inner_dim, elementwise_affine=False, eps=1e-6) self.proj_out_1 = nn.Linear(self.inner_dim, 2 * self.inner_dim) self.proj_out_2 = nn.Linear(self.inner_dim, self.config.output_dim) - print( - "Total number of DiT parameters: ", + logger.debug( + "Total number of DiT parameters: %d", sum(p.numel() for p in self.parameters() if p.requires_grad), ) @@ -318,6 +322,71 @@ class DiT(ModelMixin, ConfigMixin): return self.proj_out_2(hidden_states) +class AlternateVLDiT(DiT): + """N1.7 DiT variant that alternates cross-attention over image and text tokens.""" + + def __init__(self, *args, attend_text_every_n_blocks: int = 2, **kwargs): + super().__init__(*args, **kwargs) + self.attend_text_every_n_blocks = attend_text_every_n_blocks + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: torch.Tensor, + timestep: torch.LongTensor | None = None, + encoder_attention_mask: torch.Tensor | None = None, + return_all_hidden_states: bool = False, + image_mask: torch.Tensor | None = None, + backbone_attention_mask: torch.Tensor | None = None, + ): + if image_mask is None: + raise ValueError("image_mask is required for AlternateVLDiT.") + if backbone_attention_mask is None: + raise ValueError("backbone_attention_mask is required for AlternateVLDiT.") + + temb = self.timestep_encoder(timestep) + hidden_states = hidden_states.contiguous() + encoder_hidden_states = encoder_hidden_states.contiguous() + + image_attention_mask = image_mask & backbone_attention_mask + non_image_attention_mask = (~image_mask) & backbone_attention_mask + + all_hidden_states = [hidden_states] + if not self.config.interleave_self_attention: + raise ValueError("AlternateVLDiT requires interleave_self_attention=True.") + + for idx, block in enumerate(self.transformer_blocks): + if idx % 2 == 1: + hidden_states = block( + hidden_states, + attention_mask=None, + encoder_hidden_states=None, + encoder_attention_mask=None, + temb=temb, + ) + else: + curr_encoder_attention_mask = ( + non_image_attention_mask + if idx % (2 * self.attend_text_every_n_blocks) == 0 + else image_attention_mask + ) + hidden_states = block( + hidden_states, + attention_mask=None, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=curr_encoder_attention_mask, + temb=temb, + ) + all_hidden_states.append(hidden_states) + + conditioning = temb + shift, scale = self.proj_out_1(F.silu(conditioning)).chunk(2, dim=1) + hidden_states = self.norm_out(hidden_states) * (1 + scale[:, None]) + shift[:, None] + if return_all_hidden_states: + return self.proj_out_2(hidden_states), all_hidden_states + return self.proj_out_2(hidden_states) + + class SelfAttentionTransformer(ModelMixin, ConfigMixin): _supports_gradient_checkpointing = True @@ -362,8 +431,8 @@ class SelfAttentionTransformer(ModelMixin, ConfigMixin): for _ in range(self.config.num_layers) ] ) - print( - "Total number of SelfAttentionTransformer parameters: ", + logger.debug( + "Total number of SelfAttentionTransformer parameters: %d", sum(p.numel() for p in self.parameters() if p.requires_grad), ) diff --git a/src/lerobot/policies/groot/action_head/flow_matching_action_head.py b/src/lerobot/policies/groot/action_head/flow_matching_action_head.py deleted file mode 100644 index 986820670..000000000 --- a/src/lerobot/policies/groot/action_head/flow_matching_action_head.py +++ /dev/null @@ -1,408 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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 dataclasses import field -from typing import TYPE_CHECKING - -import torch -import torch.nn.functional as F # noqa: N812 -from torch import nn -from torch.distributions import Beta - -from lerobot.utils.import_utils import _transformers_available - -# Conditional import for type checking and lazy loading -if TYPE_CHECKING or _transformers_available: - from transformers import PretrainedConfig - from transformers.feature_extraction_utils import BatchFeature -else: - PretrainedConfig = object - BatchFeature = None - -from .action_encoder import ( - SinusoidalPositionalEncoding, - swish, -) -from .cross_attention_dit import DiT, SelfAttentionTransformer - - -class CategorySpecificLinear(nn.Module): - def __init__(self, num_categories, input_dim, hidden_dim): - super().__init__() - self.num_categories = num_categories - # For each category, we have separate weights and biases. - self.W = nn.Parameter(0.02 * torch.randn(num_categories, input_dim, hidden_dim)) - self.b = nn.Parameter(torch.zeros(num_categories, hidden_dim)) - - def forward(self, x, cat_ids): - selected_w = self.W[cat_ids] - selected_b = self.b[cat_ids] - return torch.bmm(x, selected_w) + selected_b.unsqueeze(1) - - -class CategorySpecificMLP(nn.Module): - def __init__(self, num_categories, input_dim, hidden_dim, output_dim): - super().__init__() - self.num_categories = num_categories - self.layer1 = CategorySpecificLinear(num_categories, input_dim, hidden_dim) - self.layer2 = CategorySpecificLinear(num_categories, hidden_dim, output_dim) - - def forward(self, x, cat_ids): - hidden = F.relu(self.layer1(x, cat_ids)) - return self.layer2(hidden, cat_ids) - - -class MultiEmbodimentActionEncoder(nn.Module): - def __init__(self, action_dim, hidden_size, num_embodiments): - super().__init__() - self.hidden_size = hidden_size - self.num_embodiments = num_embodiments - - # W1: R^{w x d}, W2: R^{w x 2w}, W3: R^{w x w} - self.W1 = CategorySpecificLinear(num_embodiments, action_dim, hidden_size) # (d -> w) - self.W2 = CategorySpecificLinear(num_embodiments, 2 * hidden_size, hidden_size) # (2w -> w) - self.W3 = CategorySpecificLinear(num_embodiments, hidden_size, hidden_size) # (w -> w) - self.pos_encoding = SinusoidalPositionalEncoding(hidden_size) - - def forward(self, actions, timesteps, cat_ids): - """ - actions: shape (B, T, action_dim) - timesteps: shape (B,) -- a single scalar per batch item - cat_ids: shape (B,) - returns: shape (B, T, hidden_size) - """ - b, t, _ = actions.shape - - # 1) Expand each batch's single scalar time 'tau' across all T steps - # so that shape => (B, T) - # e.g. if timesteps is (B,), replicate across T - if timesteps.dim() == 1 and timesteps.shape[0] == b: - # shape (B,) => (B,T) - timesteps = timesteps.unsqueeze(1).expand(-1, t) - else: - raise ValueError("Expected `timesteps` to have shape (B,) so we can replicate across T.") - - # 2) Standard action MLP step for shape => (B, T, w) - a_emb = self.W1(actions, cat_ids) - - # 3) Get the sinusoidal encoding (B, T, w) - tau_emb = self.pos_encoding(timesteps).to(dtype=a_emb.dtype) - - # 4) Concat along last dim => (B, T, 2w), then W2 => (B, T, w), swish - x = torch.cat([a_emb, tau_emb], dim=-1) - x = swish(self.W2(x, cat_ids)) - - # 5) Finally W3 => (B, T, w) - x = self.W3(x, cat_ids) - return x - - -class FlowmatchingActionHeadConfig(PretrainedConfig): - """NOTE: N1.5 uses XEmbFlowmatchingPolicyHeadConfig as action head""" - - add_pos_embed: bool = field(default=True, metadata={"help": "Whether to add positional embedding"}) - model_dtype: str = field(default="float32", metadata={"help": "Model data type."}) - diffusion_model_cfg: dict = field(default=None, metadata={"help": "Diffusion model configuration."}) - input_embedding_dim: int = field(default=1536, metadata={"help": "Input embedding channel dimension."}) - backbone_embedding_dim: int = field( - default=1536, metadata={"help": "Backbone embedding channel dimension."} - ) - - hidden_size: int = field(default=1024, metadata={"help": "Input embedding dimension."}) - max_seq_len: int = field(default=1024, metadata={"help": "Maximum Sequence Length"}) - action_dim: int = field(default=None, metadata={"help": "Action dimension."}) - action_horizon: int = field(default=None, metadata={"help": "Action horizon."}) - noise_beta_alpha: float = field(default=1.5, metadata={"help": ""}) - noise_beta_beta: float = field(default=1.0, metadata={"help": ""}) - noise_s: float = field(default=0.999, metadata={"help": "Flow matching noise Beta distribution s."}) - num_timestep_buckets: int = field( - default=1000, metadata={"help": "Number of timestep discretization buckets."} - ) - num_inference_timesteps: int = field( - default=None, - metadata={"help": "Number of inference steps for noise diffusion."}, - ) - max_num_embodiments: int = field(default=32, metadata={"help": "Number of embodiments."}) - tune_projector: bool = field(default=True, metadata={"help": "Whether to tune the projector."}) - tune_diffusion_model: bool = field( - default=True, metadata={"help": "Whether to tune the diffusion model."} - ) - load_pretrained_det_decode_layer_path: str = field( - default=None, metadata={"help": "Path to pretrained detection model."} - ) - detection_coeff: float = field(default=1.0, metadata={"help": "Detection coefficient."}) - - freeze_decode_layer: bool = field(default=False) - expand_batch: int = field(default=None) - use_vlln: bool = field(default=True) - - vl_self_attention_cfg: dict = field(default=None) - num_target_vision_tokens: int = field(default=32, metadata={"help": "Number of target vision tokens."}) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - for key, value in kwargs.items(): - setattr(self, key, value) - - -class FlowmatchingActionHead(nn.Module): - config_class = FlowmatchingActionHeadConfig - supports_gradient_checkpointing = True - - def __init__( - self, - config: FlowmatchingActionHeadConfig, - ): - super().__init__() - self.hidden_size = config.hidden_size - self.input_embedding_dim = config.input_embedding_dim - - self.model = DiT(**config.diffusion_model_cfg) - self.action_dim = config.action_dim - self.action_horizon = config.action_horizon - self.num_inference_timesteps = config.num_inference_timesteps - - self.state_encoder = CategorySpecificMLP( - num_categories=config.max_num_embodiments, - input_dim=config.max_state_dim, - hidden_dim=self.hidden_size, - output_dim=self.input_embedding_dim, - ) - self.action_encoder = MultiEmbodimentActionEncoder( - action_dim=config.action_dim, - hidden_size=self.input_embedding_dim, - num_embodiments=config.max_num_embodiments, - ) - self.action_decoder = CategorySpecificMLP( - num_categories=config.max_num_embodiments, - input_dim=self.hidden_size, - hidden_dim=self.hidden_size, - output_dim=self.action_dim, - ) - self.future_tokens = nn.Embedding(config.num_target_vision_tokens, self.input_embedding_dim) - nn.init.normal_(self.future_tokens.weight, mean=0.0, std=0.02) - - self.vlln = nn.LayerNorm(config.backbone_embedding_dim) if config.use_vlln else nn.Identity() - self.vl_self_attention = ( - SelfAttentionTransformer(**config.vl_self_attention_cfg) if config.use_vlln else nn.Identity() - ) - - if config.add_pos_embed: - self.position_embedding = nn.Embedding(config.max_seq_len, self.input_embedding_dim) - nn.init.normal_(self.position_embedding.weight, mean=0.0, std=0.02) - - self._noise_beta_alpha = config.noise_beta_alpha - self._noise_beta_beta = config.noise_beta_beta - self._beta_dist = None - self.num_timestep_buckets = config.num_timestep_buckets - self.config = config - self.set_trainable_parameters(config.tune_projector, config.tune_diffusion_model) - - def set_trainable_parameters(self, tune_projector: bool, tune_diffusion_model: bool): - self.tune_projector = tune_projector - self.tune_diffusion_model = tune_diffusion_model - for p in self.parameters(): - p.requires_grad = True - if not tune_projector: - self.state_encoder.requires_grad_(False) - self.action_encoder.requires_grad_(False) - self.action_decoder.requires_grad_(False) - if self.config.add_pos_embed: - self.position_embedding.requires_grad_(False) - if not tune_diffusion_model: - self.model.requires_grad_(False) - print(f"Tune action head projector: {self.tune_projector}") - print(f"Tune action head diffusion model: {self.tune_diffusion_model}") - # Check if any parameters are still trainable. If not, print a warning. - if not tune_projector and not tune_diffusion_model: - for name, p in self.named_parameters(): - if p.requires_grad: - print(f"Action head trainable parameter: {name}") - if not any(p.requires_grad for p in self.parameters()): - print("Warning: No action head trainable parameters found.") - - def set_frozen_modules_to_eval_mode(self): - """ - Huggingface will call model.train() at each training_step. To ensure - the expected behaviors for modules like dropout, batchnorm, etc., we - need to call model.eval() for the frozen modules. - """ - if self.training: - if not self.tune_projector: - self.state_encoder.eval() - self.action_encoder.eval() - self.action_decoder.eval() - if self.config.add_pos_embed: - self.position_embedding.eval() - if not self.tune_diffusion_model: - self.model.eval() - - def sample_time(self, batch_size, device, dtype): - if self._beta_dist is None: - self._beta_dist = Beta(self._noise_beta_alpha, self._noise_beta_beta, validate_args=False) - sample = self._beta_dist.sample([batch_size]).to(device, dtype=dtype) - return (self.config.noise_s - sample) / self.config.noise_s - - def prepare_input(self, batch: dict) -> BatchFeature: - return BatchFeature(data=batch) - - def process_backbone_output(self, backbone_output: BatchFeature) -> BatchFeature: - backbone_features = backbone_output["backbone_features"] - backbone_features = self.vlln(backbone_features) - backbone_features = self.vl_self_attention(backbone_features) - backbone_output["backbone_features"] = backbone_features - return backbone_output - - def forward(self, backbone_output: BatchFeature, action_input: BatchFeature) -> BatchFeature: - # Set frozen modules to eval - self.set_frozen_modules_to_eval_mode() - - backbone_output = self.process_backbone_output(backbone_output) - - if self.config.expand_batch is not None: - for k, v in backbone_output.items(): - ndim = len(v.shape) - factors = [self.config.expand_batch] - while len(factors) < ndim: - factors.append(1) - factors = tuple(factors) - expanded = v.repeat(*factors) - backbone_output[k] = expanded - - for k, v in action_input.items(): - ndim = len(v.shape) - factors = [self.config.expand_batch] - while len(factors) < ndim: - factors.append(1) - factors = tuple(factors) - expanded = v.repeat(*factors) - action_input[k] = expanded - - # Get vision and language embeddings. - vl_embs = backbone_output.backbone_features - device = vl_embs.device - - # Get embodiment ID. - embodiment_id = action_input.embodiment_id - - # Embed state. - state_features = self.state_encoder(action_input.state, embodiment_id) - - # Embed noised action trajectory. - actions = action_input.action - noise = torch.randn(actions.shape, device=actions.device, dtype=actions.dtype) - t = self.sample_time(actions.shape[0], device=actions.device, dtype=actions.dtype) - t = t[:, None, None] # shape (B,1,1) for broadcast - - noisy_trajectory = (1 - t) * noise + t * actions - velocity = actions - noise - - # Convert (continuous) t -> discrete if needed - t_discretized = (t[:, 0, 0] * self.num_timestep_buckets).long() - action_features = self.action_encoder(noisy_trajectory, t_discretized, embodiment_id) - - # Maybe add position embedding. - if self.config.add_pos_embed: - pos_ids = torch.arange(action_features.shape[1], dtype=torch.long, device=device) - pos_embs = self.position_embedding(pos_ids).unsqueeze(0) - action_features = action_features + pos_embs - - # Join vision, language, state and action embedding along sequence dimension. - future_tokens = self.future_tokens.weight.unsqueeze(0).expand(vl_embs.shape[0], -1, -1) - sa_embs = torch.cat((state_features, future_tokens, action_features), dim=1) - - vl_attn_mask = backbone_output.backbone_attention_mask - - model_output = self.model( - hidden_states=sa_embs, - encoder_hidden_states=vl_embs, - encoder_attention_mask=vl_attn_mask, - timestep=t_discretized, - return_all_hidden_states=False, # NOTE (YL): not using flare now - ) - pred = self.action_decoder(model_output, embodiment_id) - pred_actions = pred[:, -actions.shape[1] :] - - # Slice out only the action portion of pred and target. - action_mask = action_input.action_mask - loss = F.mse_loss(pred_actions, velocity, reduction="none") * action_mask - loss = loss.sum() / action_mask.sum() - output_dict = { - "loss": loss, - } - return BatchFeature(data=output_dict) - - @torch.no_grad() - def get_action(self, backbone_output: BatchFeature, action_input: BatchFeature) -> BatchFeature: - backbone_output = self.process_backbone_output(backbone_output) - - # Get vision and language embeddings. - vl_embs = backbone_output.backbone_features - embodiment_id = action_input.embodiment_id - - # Embed state. - state_features = self.state_encoder(action_input.state, embodiment_id) - - # Set initial actions as the sampled noise. - batch_size = vl_embs.shape[0] - device = vl_embs.device - actions = torch.randn( - size=(batch_size, self.config.action_horizon, self.config.action_dim), - dtype=vl_embs.dtype, - device=device, - ) - - num_steps = self.num_inference_timesteps - dt = 1.0 / num_steps - - # Run denoising steps. - for t in range(num_steps): - t_cont = t / float(num_steps) # e.g. goes 0, 1/N, 2/N, ... - t_discretized = int(t_cont * self.num_timestep_buckets) - - # Embed noised action trajectory. - timesteps_tensor = torch.full(size=(batch_size,), fill_value=t_discretized, device=device) - action_features = self.action_encoder(actions, timesteps_tensor, embodiment_id) - # Maybe add position embedding. - if self.config.add_pos_embed: - pos_ids = torch.arange(action_features.shape[1], dtype=torch.long, device=device) - pos_embs = self.position_embedding(pos_ids).unsqueeze(0) - action_features = action_features + pos_embs - - # Join vision, language, state and action embedding along sequence dimension. - future_tokens = self.future_tokens.weight.unsqueeze(0).expand(vl_embs.shape[0], -1, -1) - sa_embs = torch.cat((state_features, future_tokens, action_features), dim=1) - - # Run model forward. - model_output = self.model( - hidden_states=sa_embs, - encoder_hidden_states=vl_embs, - timestep=timesteps_tensor, - ) - pred = self.action_decoder(model_output, embodiment_id) - - pred_velocity = pred[:, -self.action_horizon :] - - # Update actions using euler integration. - actions = actions + dt * pred_velocity - return BatchFeature(data={"action_pred": actions}) - - @property - def device(self): - return next(iter(self.parameters())).device - - @property - def dtype(self): - return next(iter(self.parameters())).dtype diff --git a/src/lerobot/policies/groot/configuration_groot.py b/src/lerobot/policies/groot/configuration_groot.py index 17cb631d7..97e08bb76 100644 --- a/src/lerobot/policies/groot/configuration_groot.py +++ b/src/lerobot/policies/groot/configuration_groot.py @@ -14,12 +14,229 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +import math from dataclasses import dataclass, field +from pathlib import Path from lerobot.configs import FeatureType, NormalizationMode, PolicyFeature, PreTrainedConfig -from lerobot.optim import AdamWConfig, CosineDecayWithWarmupSchedulerConfig +from lerobot.optim import AdamWConfig, DiffuserSchedulerConfig from lerobot.utils.constants import ACTION, OBS_STATE +from .utils import read_json + +logger = logging.getLogger(__name__) + +GROOT_N1_7 = "n1.7" +# Legacy GR00T N1.5 identifier. N1.5 is NOT a supported model_version (it is +# intentionally absent from _GROOT_MODEL_VERSION_ALIASES so normalize_groot_model_version +# still rejects it). It is retained only so that infer_groot_model_version can recognise +# an N1.5 base path/checkpoint and the N1.7 config/loader can reject the mismatch. +GROOT_N1_5 = "n1.5" +# Canonical guidance appended to every error raised when an N1.5 checkpoint, config, +# or processor pipeline is detected. Keep this message in sync with docs/source/groot.mdx. +GROOT_N1_5_REMOVAL_GUIDANCE = ( + "GR00T N1.5 support was removed from LeRobot. " + "To keep using an N1.5 checkpoint, pin the last release that supports it: " + "`pip install 'lerobot==0.5.1'`. To use the current release, migrate to GR00T N1.7 " + "(model_version='n1.7', base model nvidia/GR00T-N1.7-3B)." +) +GROOT_N1_7_BASE_MODEL = "nvidia/GR00T-N1.7-3B" +GROOT_N1_7_BACKBONE_MODEL = "nvidia/Cosmos-Reason2-2B" +# Default GR00T N1.7 training resolution. Fallback if processor_config lacks sizing. Prevents mismatched +# full-res patchification by forcing a resize. Mirrored by GR00T_N1_7_DEFAULTS in groot_n1_7.py. +N1_7_DEFAULT_IMAGE_TARGET_SIZE = (256, 256) +N1_7_DEFAULT_IMAGE_CROP_SIZE = (230, 230) +GROOT_ACTION_DECODE_TRANSFORM_LIBERO = "libero" +# Sentinel meaning "the user did not pick an action decode transform": __post_init__ resolves it +# to the embodiment default ('libero' for 'libero_sim', otherwise None). It is distinct from an +# explicit 'none' (resolved to None) so an opt-out survives a draccus save/load round-trip. +GROOT_ACTION_DECODE_TRANSFORM_AUTO = "auto" + +_GROOT_MODEL_VERSION_ALIASES = { + "n1.7": GROOT_N1_7, + "n1_7": GROOT_N1_7, + "n1d7": GROOT_N1_7, + "n17": GROOT_N1_7, + "1.7": GROOT_N1_7, +} + +# Legacy N1.5 spellings, kept ONLY so they can be detected and rejected with +# GROOT_N1_5_REMOVAL_GUIDANCE (see GROOT_N1_5 above). Never map these to a supported version. +_GROOT_N1_5_VERSION_ALIASES = {"n1.5", "n1_5", "n1d5", "n15", "1.5"} + +_GROOT_ACTION_DECODE_TRANSFORM_ALIASES = { + GROOT_ACTION_DECODE_TRANSFORM_AUTO: GROOT_ACTION_DECODE_TRANSFORM_AUTO, + "none": None, + "": None, + GROOT_ACTION_DECODE_TRANSFORM_LIBERO: GROOT_ACTION_DECODE_TRANSFORM_LIBERO, +} + + +def normalize_groot_model_version(model_version: str) -> str: + normalized = _GROOT_MODEL_VERSION_ALIASES.get(model_version.lower()) + if normalized is None: + supported = GROOT_N1_7 + message = f"Unsupported GR00T model_version '{model_version}'. Supported versions: {supported}." + if model_version.lower() in _GROOT_N1_5_VERSION_ALIASES: + message = f"{message} {GROOT_N1_5_REMOVAL_GUIDANCE}" + raise ValueError(message) + return normalized + + +def normalize_groot_action_decode_transform(transform: str | None) -> str | None: + if transform is None: + return None + normalized = _GROOT_ACTION_DECODE_TRANSFORM_ALIASES.get(transform.lower()) + if normalized is None and transform.lower() not in _GROOT_ACTION_DECODE_TRANSFORM_ALIASES: + supported = ", ".join( + sorted(key for key, value in _GROOT_ACTION_DECODE_TRANSFORM_ALIASES.items() if value is not None) + ) + raise ValueError( + f"Unsupported GR00T N1.7 action decode transform '{transform}'. " + f"Supported transforms: none, {supported}." + ) + return normalized + + +def infer_groot_model_version(model_path: str | None) -> str | None: + if not model_path: + return None + model_path_lower = model_path.lower() + if "gr00t-n1.7" in model_path_lower or "gr00t_n1.7" in model_path_lower: + return GROOT_N1_7 + # Detect legacy N1.5 paths so the N1.7 config/loader can reject the mismatch. + # N1.5 is unsupported, but it must still be recognised here to fail loudly + # rather than silently treating an N1.5 checkpoint as N1.7. + if "gr00t-n1.5" in model_path_lower or "gr00t_n1.5" in model_path_lower: + return GROOT_N1_5 + config_version = _infer_groot_model_version_from_local_config(model_path) + if config_version is not None: + return config_version + return None + + +def is_raw_groot_n1_7_checkpoint(model_path: str | Path | None) -> bool: + if model_path is None: + return False + + path = Path(model_path).expanduser() + if path.is_dir(): + config_path = path / "config.json" + elif path.name == "config.json": + config_path = path + else: + return False + + config = read_json(config_path) + return "type" not in config and _infer_groot_model_version_from_config(config) == GROOT_N1_7 + + +def infer_groot_n1_7_embodiment_tag(model_path: str | Path | None) -> str | None: + if model_path is None: + return None + + processor_config_path = Path(model_path).expanduser() / "processor_config.json" + processor_config = read_json(processor_config_path) + + modality_configs = processor_config.get("processor_kwargs", {}).get("modality_configs", {}) + if not isinstance(modality_configs, dict): + return None + if "libero_sim" in modality_configs: + return "libero_sim" + if len(modality_configs) == 1: + return next(iter(modality_configs)) + return None + + +def infer_groot_n1_7_action_horizon( + model_path: str | Path | None, embodiment_tag: str | None = None +) -> int | None: + if model_path is None: + return None + + processor_config_path = Path(model_path).expanduser() / "processor_config.json" + processor_config = read_json(processor_config_path) + + processor_kwargs = processor_config.get("processor_kwargs", {}) + if not isinstance(processor_kwargs, dict): + return None + modality_configs = processor_kwargs.get("modality_configs", {}) + if not isinstance(modality_configs, dict): + return None + + if embodiment_tag is None: + embodiment_tag = infer_groot_n1_7_embodiment_tag(model_path) + if embodiment_tag is None: + return None + + embodiment_config = modality_configs.get(embodiment_tag, {}) + if not isinstance(embodiment_config, dict): + return None + action_config = embodiment_config.get("action", {}) + if not isinstance(action_config, dict): + return None + delta_indices = action_config.get("delta_indices", []) + if not isinstance(delta_indices, list): + return None + return len(delta_indices) or None + + +def infer_groot_n1_7_action_execution_horizon( + model_path: str | Path | None, embodiment_tag: str | None = None +) -> int | None: + action_horizon = infer_groot_n1_7_action_horizon(model_path, embodiment_tag) + if action_horizon is None: + return None + + if embodiment_tag is None: + embodiment_tag = infer_groot_n1_7_embodiment_tag(model_path) + if embodiment_tag == "libero_sim": + # NVIDIA's N1.7 LIBERO rollout wrapper replans after 8 of the 16 decoded + # actions. Keeping that execution cadence avoids stale open-loop chunks. + return min(action_horizon, 8) + return action_horizon + + +def _infer_groot_model_version_from_local_config(model_path: str) -> str | None: + path = Path(model_path).expanduser() + if path.is_dir(): + config_path = path / "config.json" + elif path.name == "config.json": + config_path = path + else: + return None + + return _infer_groot_model_version_from_config(read_json(config_path)) + + +def _infer_groot_model_version_from_config(config: dict) -> str | None: + model_version = config.get("model_version") + if isinstance(model_version, str): + if model_version.lower() in _GROOT_N1_5_VERSION_ALIASES: + return GROOT_N1_5 + try: + return normalize_groot_model_version(model_version) + except ValueError: + return None + + candidates = [config.get("model_type"), *(config.get("architectures") or [])] + for candidate in candidates: + if not isinstance(candidate, str): + continue + normalized = candidate.lower().replace("-", "_") + if normalized in {"gr00tn1d7", "gr00t_n1d7", "gr00t_n1_7"}: + return GROOT_N1_7 + if normalized in {"gr00t_n1_5", "gr00tn1_5", "gr00t_n15", "gr00t_n1d5", "gr00tn1d5"}: + return GROOT_N1_5 + if config.get("model_name") == GROOT_N1_7_BACKBONE_MODEL: + return GROOT_N1_7 + # The Eagle VLM backbone is specific to pre-N1.7 GR00T checkpoints (N1.7 uses Cosmos/Qwen3-VL). + backbone_cfg = config.get("backbone_cfg") + if isinstance(backbone_cfg, dict) and "eagle_path" in backbone_cfg: + return GROOT_N1_5 + return None + @PreTrainedConfig.register_subclass("groot") @dataclass @@ -28,35 +245,44 @@ class GrootConfig(PreTrainedConfig): # Basic policy settings n_obs_steps: int = 1 - chunk_size: int = 50 - n_action_steps: int = 50 + chunk_size: int = 40 + n_action_steps: int = 40 # Dimension settings (must match pretrained GR00T model expectations) # Maximum state dimension. Shorter states will be zero-padded. - max_state_dim: int = 64 + max_state_dim: int = 132 # Maximum action dimension. Shorter actions will be zero-padded. - max_action_dim: int = 32 + max_action_dim: int = 132 - # Normalization (start with identity, adjust as needed) + # GR00T normalizes state/action internally in its processor steps (min/max with + # q01/q99 percentiles, per embodiment), and the Qwen3-VL backbone's image processor + # handles image normalization. The policy therefore does NOT use LeRobot's + # NormalizerProcessorStep/UnnormalizerProcessorStep, so this mapping is intentionally + # IDENTITY for every feature and is not consulted by make_groot_pre_post_processors. normalization_mapping: dict[str, NormalizationMode] = field( default_factory=lambda: { "VISUAL": NormalizationMode.IDENTITY, - "STATE": NormalizationMode.MEAN_STD, - "ACTION": NormalizationMode.MEAN_STD, + "STATE": NormalizationMode.IDENTITY, + "ACTION": NormalizationMode.IDENTITY, } ) - # Image preprocessing (adjust to match Groot's expected input) - image_size: tuple[int, int] = (224, 224) + # Groot-specific model parameters - # Groot-specific model parameters (from groot_finetune_script.py) + # Path or HuggingFace model ID for the base GR00T N1.7 model whose backbone weights and + # checkpoint sidecars (statistics.json, processor_config.json, ...) are loaded. This is the + # model *source*, and is intentionally distinct from the inherited `pretrained_path`: + # `pretrained_path` (`--policy.path`) points at a saved LeRobot checkpoint directory whose + # `config.json` carries a `type` field, whereas a raw NVIDIA GR00T checkpoint has no such + # field and so can only be loaded through `base_model_path` (`--policy.base_model_path`). + # Defaults to GROOT_N1_7_BASE_MODEL when unset (resolved in __post_init__). + base_model_path: str | None = None - # Path or HuggingFace model ID for the base Groot model - base_model_path: str = "nvidia/GR00T-N1.5-3B" - - # HF repo ID (or local path) that hosts vocab.json and merges.txt for Eagle tokenizer. - tokenizer_assets_repo: str = "lerobot/eagle2hg-processor-groot-n1p5" + # Optional named action transform applied after raw N1.7 checkpoint decoding and before env.step(). + # 'auto' (default) resolves to the embodiment default ('libero' for 'libero_sim', otherwise no + # transform). Pass 'none' to explicitly disable the transform, including for 'libero_sim'. + action_decode_transform: str | None = GROOT_ACTION_DECODE_TRANSFORM_AUTO # Embodiment tag to use for training (e.g. 'new_embodiment', 'gr1') embodiment_tag: str = "new_embodiment" @@ -75,38 +301,67 @@ class GrootConfig(PreTrainedConfig): # Whether to fine-tune the diffusion model tune_diffusion_model: bool = True - # LoRA parameters (from groot_finetune_script.py) - # Rank for the LORA model. If 0, no LORA will be used. - lora_rank: int = 0 + # Whether to fine-tune the VL LayerNorm + VL self-attention projector in the action head. + tune_vlln: bool = True - # Alpha value for the LORA model - lora_alpha: int = 16 + # Number of top LLM backbone layers to fine-tune (0 = none). Lets you adapt just the final + # language layers without unfreezing the whole backbone; independent of `tune_llm`, which tunes + # the entire LLM. + tune_top_llm_layers: int = 0 - # Dropout rate for the LORA model - lora_dropout: float = 0.1 + # Inference-time knob: Number of flow-matching denoising steps used to decode an action chunk. + # Trades inference latency for action quality. + # None keeps the checkpoint value (GR00T N1.7 default: 4). + num_inference_timesteps: int | None = None - # Whether to use the full model for LORA - lora_full_model: bool = False + # Inference-time knob: Real-Time Chunking (RTC) overlap-blend ramp rate, used when the RTC engine + # supplies a previous-chunk prefix. Higher values blend the overlapping prefix more aggressively. + # None keeps the checkpoint value (GR00T N1.7 default: 6.0). + rtc_ramp_rate: float | None = None - # Training parameters (matching groot_finetune_script.py) + # Inference-time knob: Whether to request the flash-attention-2 kernel for the Qwen3-VL backbone. + # flash-attn is an optional, user-managed optimization; when it is absent (the default), + # the backbone transparently falls back to SDPA, which is numerically equivalent. + # Set to True only after installing a flash-attn build matching your torch/CUDA env. + use_flash_attention: bool = False + + # Enable GR00T-style state-relative action chunks (action chunk expressed relative to the current + # observation state). + use_relative_actions: bool = False + + # relative_exclude_joints names the action dimensions that stay absolute; the + # match is substring/case-insensitive against the dataset action feature names. With the empty + # default every dimension is treated as relative, including the gripper -- set e.g. ["gripper"] to + # keep the gripper absolute, matching the Isaac-GR00T single-arm + absolute-gripper convention. + relative_exclude_joints: list[str] = field(default_factory=list) + + # Training parameters optimizer_lr: float = 1e-4 - optimizer_betas: tuple[float, float] = (0.95, 0.999) + # Isaac-GR00T N1.7 fine-tunes with AdamW betas (0.9, 0.999). + optimizer_betas: tuple[float, float] = (0.9, 0.999) optimizer_eps: float = 1e-8 optimizer_weight_decay: float = 1e-5 warmup_ratio: float = 0.05 use_bf16: bool = True + # The native N1.7 fine-tuning recipe keeps model parameters in FP32 and computes under BF16 autocast. + model_params_fp32: bool = True - # Dataset parameters - # Video backend to use for training ('decord' or 'torchvision_av') + # TODO(Steven): Remove these deprecated fields in a future release. + # Deprecated Isaac-GR00T runner / GR00T N1.5 fields, plus the (never-wired) LoRA fields — all + # unused by the LeRobot N1.7 implementation except the `tokenizer_assets_repo` N1.5 tripwire and + # the `image_size` legacy remap in __post_init__. They are kept ONLY so a config.json saved by an + # earlier lerobot release (notably a GR00T N1.5 checkpoint) still parses under draccus — which + # rejects unknown fields — and is then rejected with a clear N1.5 removal message rather than an + # opaque draccus decoding error. + image_size: tuple[int, int] = (256, 256) # image sizing is handled by the backbone's image processor. + tokenizer_assets_repo: str | None = None + lora_rank: int = 0 + lora_alpha: int = 16 + lora_dropout: float = 0.1 + lora_full_model: bool = False video_backend: str = "decord" - - # Whether to balance dataset weights in mixture datasets balance_dataset_weights: bool = True - - # Whether to sample trajectories weighted by their length balance_trajectory_weights: bool = True - - # Optional dataset paths for delegating training to Isaac-GR00T runner dataset_paths: list[str] | None = None output_dir: str = "./tmp/gr00t" save_steps: int = 1000 @@ -117,6 +372,65 @@ class GrootConfig(PreTrainedConfig): resume: bool = False def __post_init__(self): + if self.tokenizer_assets_repo is not None: + raise ValueError( + "Config sets 'tokenizer_assets_repo', which only existed for GR00T N1.5; this looks " + f"like a legacy GR00T N1.5 checkpoint or config. {GROOT_N1_5_REMOVAL_GUIDANCE}" + ) + + self.action_decode_transform = normalize_groot_action_decode_transform(self.action_decode_transform) + if self.base_model_path is None: + self.base_model_path = GROOT_N1_7_BASE_MODEL + + # The N1.7 LIBERO checkpoints emit a [0, 1] gripper action, but the LIBERO + # simulator expects the OpenVLA/[-1, 1] sign convention. NVIDIA's rollout + # wrapper applies this conversion; mirror it here so eval on the + # 'libero_sim' embodiment grasps correctly instead of scoring 0% success. + # This matches the embodiment-specific handling already done for the + # action execution horizon (see infer_groot_n1_7_action_execution_horizon). + # Only the 'auto' sentinel resolves to the embodiment default; an explicit + # 'none' (normalized to None above) keeps the transform disabled. + if self.action_decode_transform == GROOT_ACTION_DECODE_TRANSFORM_AUTO: + self.action_decode_transform = ( + GROOT_ACTION_DECODE_TRANSFORM_LIBERO if self.embodiment_tag == "libero_sim" else None + ) + + # GR00T N1.5-era default values (e.g. --policy.chunk_size=50 from old commands or + # stale configs) are migrated to the values the N1.7 checkpoints expect, with a + # warning. The dataclass defaults are already the N1.7 values, so a plain + # GrootConfig() never triggers this. + legacy_default_remaps = ( + ("max_state_dim", 64, 132), + ("max_action_dim", 32, 132), + ("chunk_size", 50, 40), + ("n_action_steps", 50, 40), + ("image_size", (224, 224), (256, 256)), + ) + for field_name, legacy_value, n1_7_value in legacy_default_remaps: + current_value = getattr(self, field_name) + if isinstance(legacy_value, tuple): + current_value = tuple(current_value) + if current_value == legacy_value: + logger.warning( + "GrootConfig.%s=%s matches a legacy GR00T N1.5-era default; remapping it to %s, " + "the value expected by GR00T N1.7 checkpoints. Set a different value explicitly " + "if this is not what you want.", + field_name, + legacy_value, + n1_7_value, + ) + setattr(self, field_name, n1_7_value) + + inferred_version = infer_groot_model_version(self.base_model_path) + if inferred_version is not None and inferred_version != GROOT_N1_7: + message = ( + f"GR00T model_version '{GROOT_N1_7}' does not match base_model_path " + f"'{self.base_model_path}', which looks like '{inferred_version}'." + ) + if inferred_version == GROOT_N1_5: + message = f"{message} {GROOT_N1_5_REMOVAL_GUIDANCE}" + raise ValueError(message) + super().__post_init__() if self.n_action_steps > self.chunk_size: @@ -124,9 +438,6 @@ class GrootConfig(PreTrainedConfig): f"n_action_steps ({self.n_action_steps}) cannot exceed chunk_size ({self.chunk_size})" ) - # groot_repo_path is now optional since we ported the components - # No validation needed - def validate_features(self) -> None: """Validate and set up input/output features for Groot.""" image_features = [key for key, feat in self.input_features.items() if feat.type == FeatureType.VISUAL] @@ -173,15 +484,20 @@ class GrootConfig(PreTrainedConfig): betas=self.optimizer_betas, eps=self.optimizer_eps, weight_decay=self.optimizer_weight_decay, + grad_clip_norm=1.0, ) - def get_scheduler_preset(self) -> CosineDecayWithWarmupSchedulerConfig: - """Return scheduler configuration.""" - return CosineDecayWithWarmupSchedulerConfig( - num_warmup_steps=int(10000 * self.warmup_ratio), # 5% warmup by default - num_decay_steps=10000, # Adjust based on training steps - peak_lr=self.optimizer_lr, - decay_lr=self.optimizer_lr * 0.1, + def get_scheduler_preset(self) -> DiffuserSchedulerConfig: + """Return scheduler configuration. + + Isaac-GR00T uses the HF Trainer cosine schedule with ~5% warmup over the + actual training update count; DiffuserSchedulerConfig wraps the same + diffusers/transformers `get_scheduler("cosine")` implementation and + derives num_training_steps from the outer --steps value at runtime. + """ + return DiffuserSchedulerConfig( + name="cosine", + num_warmup_steps=math.ceil(self.max_steps * self.warmup_ratio), ) @property @@ -192,7 +508,15 @@ class GrootConfig(PreTrainedConfig): @property def action_delta_indices(self) -> list[int]: """Return indices for delta actions.""" - return list(range(min(self.chunk_size, 16))) + model_action_horizon = ( + infer_groot_n1_7_action_horizon(self.base_model_path, self.embodiment_tag) or 40 + ) + return list(range(min(self.chunk_size, model_action_horizon))) + + @property + def drop_n_last_frames(self) -> int: + """Exclude episode tails that cannot supply a complete N1.7 action chunk.""" + return max(0, len(self.action_delta_indices) - 1) @property def reward_delta_indices(self) -> None: diff --git a/src/lerobot/policies/groot/eagle2_hg_model/configuration_eagle2_5_vl.py b/src/lerobot/policies/groot/eagle2_hg_model/configuration_eagle2_5_vl.py deleted file mode 100755 index 526b4f7a2..000000000 --- a/src/lerobot/policies/groot/eagle2_hg_model/configuration_eagle2_5_vl.py +++ /dev/null @@ -1,135 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# 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 copy - -from transformers.configuration_utils import PretrainedConfig -from transformers.models.llama.configuration_llama import LlamaConfig -from transformers.models.qwen2.configuration_qwen2 import Qwen2Config -from transformers.models.qwen3.configuration_qwen3 import Qwen3Config -from transformers.models.siglip.configuration_siglip import SiglipVisionConfig -from transformers.utils import logging - -logger = logging.get_logger(__name__) - - -class Eagle25VLConfig(PretrainedConfig): - model_type = "eagle_2_5_vl" - is_composition = True - sub_configs = {"vision_config": SiglipVisionConfig, "text_config": Qwen2Config} - - def __init__( - self, - vision_config=None, - text_config=None, - use_backbone_lora=0, - use_llm_lora=0, - pad2square=False, - select_layer=-4, - force_image_size=None, - downsample_ratio=0.5, - template=None, - dynamic_image_size=False, - use_thumbnail=False, - loss_version="v1", - min_dynamic_tiles=1, - max_dynamic_tiles=6, - mlp_checkpoint=False, - initializer_range=0.02, - _attn_implementation="flash_attention_2", - _attn_implementation_autoset=False, - llm_config=None, - image_token_index=None, - use_pixel_shuffle=True, - mlp_connector_layers=2, - **kwargs, - ): - super().__init__(**kwargs) - - if vision_config is None: - vision_config = {"model_type": "siglip_vision_model"} - logger.info("vision_config is None. Initializing the InternVisionConfig with default values.") - - if text_config is None: - text_config = {"architectures": ["Qwen2ForCausalLM"]} - logger.info( - "text_config is None. Initializing the LlamaConfig config with default values (`LlamaConfig`)." - ) - - if vision_config["model_type"] == "siglip_vision_model": - self.vision_config = SiglipVisionConfig(**vision_config) - else: - raise ValueError("Unsupported model_type: {}".format(vision_config["model_type"])) - - if text_config["architectures"][0] == "LlamaForCausalLM": - self.text_config = LlamaConfig(**text_config) - elif text_config["architectures"][0] == "Qwen2ForCausalLM": - self.text_config = Qwen2Config(**text_config) - elif text_config["architectures"][0] == "Qwen3ForCausalLM": - self.text_config = Qwen3Config(**text_config) - else: - raise ValueError("Unsupported architecture: {}".format(text_config["architectures"][0])) - self.use_backbone_lora = use_backbone_lora - self.use_llm_lora = use_llm_lora - self.mlp_checkpoint = mlp_checkpoint - self.pad2square = pad2square - self.select_layer = select_layer - self.force_image_size = force_image_size - self.downsample_ratio = downsample_ratio - self.template = template - self.dynamic_image_size = dynamic_image_size - self.use_thumbnail = use_thumbnail - self.loss_version = loss_version - self.initializer_range = initializer_range - self.min_dynamic_tiles = min_dynamic_tiles - self.max_dynamic_tiles = max_dynamic_tiles - self.tie_word_embeddings = self.text_config.tie_word_embeddings - self._attn_implementation = _attn_implementation - self._attn_implementation_autoset = _attn_implementation_autoset - self.image_token_index = image_token_index - self.use_pixel_shuffle = use_pixel_shuffle - self.mlp_connector_layers = mlp_connector_layers - logger.info(f"min_dynamic_tiles: {self.min_dynamic_tiles}") - logger.info(f"max_dynamic_tiles: {self.max_dynamic_tiles}") - - def to_dict(self): - """ - Serializes this instance to a Python dictionary. Override the default [`~PretrainedConfig.to_dict`]. - - Returns: - `Dict[str, any]`: Dictionary of all the attributes that make up this configuration instance, - """ - output = copy.deepcopy(self.__dict__) - output["vision_config"] = self.vision_config.to_dict() - output["text_config"] = self.text_config.to_dict() - output["model_type"] = self.__class__.model_type - output["use_backbone_lora"] = self.use_backbone_lora - output["use_llm_lora"] = self.use_llm_lora - output["pad2square"] = self.pad2square - output["select_layer"] = self.select_layer - output["force_image_size"] = self.force_image_size - output["downsample_ratio"] = self.downsample_ratio - output["template"] = self.template - output["dynamic_image_size"] = self.dynamic_image_size - output["use_thumbnail"] = self.use_thumbnail - output["min_dynamic_tiles"] = self.min_dynamic_tiles - output["max_dynamic_tiles"] = self.max_dynamic_tiles - output["tie_word_embeddings"] = self.tie_word_embeddings - output["_attn_implementation"] = self._attn_implementation - output["_attn_implementation_autoset"] = self._attn_implementation_autoset - output["use_pixel_shuffle"] = self.use_pixel_shuffle - output["mlp_connector_layers"] = self.mlp_connector_layers - return output diff --git a/src/lerobot/policies/groot/eagle2_hg_model/image_processing_eagle2_5_vl_fast.py b/src/lerobot/policies/groot/eagle2_hg_model/image_processing_eagle2_5_vl_fast.py deleted file mode 100644 index 90e9dcecc..000000000 --- a/src/lerobot/policies/groot/eagle2_hg_model/image_processing_eagle2_5_vl_fast.py +++ /dev/null @@ -1,503 +0,0 @@ -# -------------------------------------------------------- -# NVIDIA -# Copyright (c) 2025 NVIDIA -# Licensed under The MIT License [see LICENSE for details] -# -------------------------------------------------------- - -from __future__ import annotations - -# copy from https://github.com/huggingface/transformers/blob/main/src/transformers/models/llava_onevision/image_processing_llava_onevision_fast.py -from transformers.image_processing_utils import ( - BatchFeature, - get_patch_output_size, -) -from transformers.image_processing_utils_fast import ( - BaseImageProcessorFast, - ImagesKwargs, - group_images_by_shape, - reorder_images, -) -from transformers.image_utils import ( - IMAGENET_STANDARD_MEAN, # 0.5, 0.5, 0.5 - IMAGENET_STANDARD_STD, # 0.5, 0.5, 0.5 - ChannelDimension, - ImageInput, - PILImageResampling, - SizeDict, - get_image_size, - make_flat_list_of_images, - validate_kwargs, -) -from transformers.processing_utils import Unpack -from transformers.utils import ( - TensorType, - add_start_docstrings, - is_torch_available, - is_torchvision_v2_available, -) -from transformers.video_utils import VideoInput - -if is_torch_available(): - import torch -if is_torchvision_v2_available(): - from torchvision.transforms.v2 import functional as F # noqa: N812 - from transformers.image_utils import pil_torch_interpolation_mapping -else: - from torchvision.transforms import functional as F # noqa: N812 - - -def crop(img: torch.Tensor, left: int, top: int, right: int, bottom: int) -> torch.Tensor: - """Crop the given numpy array. - - Args: - img (torch.Tensor): Image to be cropped. Format should be (C, H, W). - left (int): The left coordinate of the crop box. - top (int): The top coordinate of the crop box. - right (int): The right coordinate of the crop box. - bottom (int): The bottom coordinate of the crop box. - - Returns: - torch.Tensor: Cropped image. - """ - if not isinstance(img, torch.Tensor): - raise TypeError(f"img should be torch.Tensor. Got {type(img)}") - - if img.ndim not in [2, 3]: - raise ValueError(f"Image should have 2 or 3 dimensions. Got {img.ndim}") - - img_height = img.shape[1] - img_width = img.shape[2] - if top < 0 or left < 0 or bottom > img_height or right > img_width: - raise ValueError("Crop coordinates out of bounds") - - if top >= bottom or left >= right: - raise ValueError("Invalid crop coordinates") - - return img[:, top:bottom, left:right] - - -class Eagle25VLFastImageProcessorKwargs(ImagesKwargs): - max_dynamic_tiles: int | None - min_dynamic_tiles: int | None - use_thumbnail: bool | None - pad_during_tiling: bool | None - do_pad: bool | None - - -@add_start_docstrings( - "Constructs a fast ConvNeXT image processor. Based on [`SiglipImageProcessor`] with incorporation of processing each video frame.", - # BASE_IMAGE_PROCESSOR_FAST_DOCSTRING, TODO: this was depreciated from transformers remove! - """ - image_grid_pinpoints (`List[List[int]]`, *optional*): - A list of possible resolutions to use for processing high resolution images. The best resolution is selected - based on the original size of the image. Can be overridden by `image_grid_pinpoints` in the `preprocess` - method. Not used for processing videos. - do_pad (`bool`, *optional*): - Whether to pad the image. If `True`, will pad the patch dimension of the images in the batch to the largest - number of patches in the batch. Padding will be applied to the bottom and right with zeros. - """, -) -class Eagle25VLImageProcessorFast(BaseImageProcessorFast): - resample = PILImageResampling.BICUBIC - image_mean = IMAGENET_STANDARD_MEAN - image_std = IMAGENET_STANDARD_STD - size = {"height": 448, "width": 448} - default_to_square = False - crop_size = None - do_resize = True - do_center_crop = None - do_rescale = True - do_normalize = True - do_convert_rgb = True - do_pad = True - max_dynamic_tiles = 12 - min_dynamic_tiles = 1 - use_thumbnail = True - pad_during_tiling = False - valid_kwargs = Eagle25VLFastImageProcessorKwargs - model_input_names = ["pixel_values_videos"] - - def __init__(self, **kwargs: Unpack[Eagle25VLFastImageProcessorKwargs]): - super().__init__(**kwargs) - - @add_start_docstrings( - # BASE_IMAGE_PROCESSOR_FAST_DOCSTRING_PREPROCESS, TODO: this was depreciated from transformers remove! - """ - max_dynamic_tiles (`int`, *optional*): - The maximum number of dynamic tiles to use for processing high resolution images. - min_dynamic_tiles (`int`, *optional*): - The minimum number of dynamic tiles to use for processing high resolution images. - use_thumbnail (`bool`, *optional*): - Whether to use a thumbnail for processing high resolution images. - pad_during_tiling (`bool`, *optional*): - Whether to pad the image during tiling. - do_pad (`bool`, *optional*): - Whether to pad the image. If `True`, will pad the patch dimension of the images in the batch to the largest - number of patches in the batch. Padding will be applied to the bottom and right with zeros. - """, - ) - - # NOTE(YL): we will overload the preprocess method to add the image_flags - # def preprocess( - # self, images: ImageInput, **kwargs: Unpack[Eagle25VLFastImageProcessorKwargs] - # ) -> BatchFeature: - # return super().preprocess(images, **kwargs) - - def _prepare_images_structure( - self, - images: ImageInput, - expected_ndims: int = 3, - ) -> ImageInput: - """ - Prepare the images structure for processing. - - Args: - images (`ImageInput`): - The input images to process. - expected_ndims (`int`, *optional*, defaults to 3): - Expected number of dimensions for the images (added for transformers >=4.53.0 compatibility). - - Returns: - `ImageInput`: The images with a valid nesting. - """ - return make_flat_list_of_images(images) - - def _resize_for_patching( - self, - image: torch.Tensor, - target_resolution: tuple, - interpolation: F.InterpolationMode, - input_data_format: ChannelDimension, - ) -> torch.Tensor: - """ - Resizes an image to a target resolution while maintaining aspect ratio. - - Args: - image ("torch.Tensor"): - The input image. - target_resolution (tuple): - The target resolution (height, width) of the image. - interpolation (`InterpolationMode`): - Resampling filter to use if resizing the image. - input_data_format (`ChannelDimension` or `str`): - The channel dimension format of the input image. - - Returns: - "torch.Tensor": The resized and padded image. - """ - new_height, new_width = get_patch_output_size(image, target_resolution, input_data_format) - - # Resize the image - resized_image = F.resize(image, (new_height, new_width), interpolation=interpolation) - - return resized_image - - def find_closest_aspect_ratio(self, aspect_ratio, target_ratios, width, height, image_size): - """ - previous version mainly focus on ratio. - We also consider area ratio here. - """ - best_factor = float("-inf") - best_ratio = (1, 1) - area = width * height - for ratio in target_ratios: - target_aspect_ratio = ratio[0] / ratio[1] - # ratio_diff = abs(aspect_ratio - target_aspect_ratio) - # area_ratio = (ratio[0] * ratio[1] * image_size * image_size) / area - """ - new area > 60% of original image area is enough. - """ - factor_based_on_area_n_ratio = min( - (ratio[0] * ratio[1] * image_size * image_size) / area, 0.6 - ) * min(target_aspect_ratio / aspect_ratio, aspect_ratio / target_aspect_ratio) - - if factor_based_on_area_n_ratio > best_factor: - best_factor = factor_based_on_area_n_ratio - best_ratio = ratio - - return best_ratio - - def _pad_for_patching( - self, image: torch.Tensor, target_resolution: tuple, input_data_format: ChannelDimension - ) -> torch.Tensor: - """ - Pad an image to a target resolution while maintaining aspect ratio. - """ - target_height, target_width = target_resolution - new_height, new_width = get_patch_output_size(image, target_resolution, input_data_format) - - paste_x = (target_width - new_width) // 2 - paste_y = (target_height - new_height) // 2 - - padded_image = F.pad(image, padding=[paste_x, paste_y, paste_x, paste_y]) - - return padded_image - - def _get_image_patches( - self, - image: torch.Tensor, - min_num: int, - max_num: int, - size: tuple, - tile_size: int, - use_thumbnail: bool, - interpolation: F.InterpolationMode, - pad_during_tiling: bool, - ) -> list[torch.Tensor]: - image_size = get_image_size(image, channel_dim=ChannelDimension.FIRST) - orig_height, orig_width = image_size - aspect_ratio = orig_width / orig_height - - # calculate the existing image aspect ratio - target_ratios = { - (i, j) - for n in range(min_num, max_num + 1) - for i in range(1, n + 1) - for j in range(1, n + 1) - if i * j <= max_num and i * j >= min_num - } - target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1]) - - # find the closest aspect ratio to the target - target_aspect_ratio = self.find_closest_aspect_ratio( - aspect_ratio, target_ratios, orig_width, orig_height, tile_size - ) - - # calculate the target width and height - target_width = tile_size * target_aspect_ratio[0] - target_height = tile_size * target_aspect_ratio[1] - blocks = target_aspect_ratio[0] * target_aspect_ratio[1] - if pad_during_tiling: - resized_image = self._resize_for_patching( - image, - (target_height, target_width), - interpolation=interpolation, - input_data_format=ChannelDimension.FIRST, - ) - padded_image = self._pad_for_patching( - resized_image, - (target_height, target_width), - input_data_format=ChannelDimension.FIRST, - ) - image_used_to_split = padded_image - else: - image_used_to_split = F.resize(image, (target_height, target_width), interpolation=interpolation) - - processed_tiles = [] - for i in range(blocks): - box = ( - (i % (target_width // tile_size)) * tile_size, - (i // (target_width // tile_size)) * tile_size, - ((i % (target_width // tile_size)) + 1) * tile_size, - ((i // (target_width // tile_size)) + 1) * tile_size, - ) - # split the image - split_img = crop(image_used_to_split, box[0], box[1], box[2], box[3]) - processed_tiles.append(split_img) - assert len(processed_tiles) == blocks - - if use_thumbnail and len(processed_tiles) != 1: - thumbnail_img = F.resize(image, (tile_size, tile_size), interpolation=interpolation) - processed_tiles.append(thumbnail_img) - - return processed_tiles - - def _pad_for_batching( - self, - pixel_values: list[torch.Tensor], - ) -> list[torch.Tensor]: - """ - Pads images on the `num_of_patches` dimension with zeros to form a batch of same number of patches. - - Args: - pixel_values (`List[torch.Tensor]`): - An array of pixel values of each images of shape (`batch_size`, `num_patches`, `image_in_3D`) - - Returns: - List[`torch.Tensor`]: The padded images. - """ - max_patch = max(len(x) for x in pixel_values) - pixel_values = [ - torch.nn.functional.pad(image, pad=[0, 0, 0, 0, 0, 0, 0, max_patch - image.shape[0]]) - for image in pixel_values - ] - - return pixel_values - - def _preprocess( - self, - images: list[torch.Tensor], - do_resize: bool, - size: SizeDict, - max_dynamic_tiles: int, - min_dynamic_tiles: int, - use_thumbnail: bool, - pad_during_tiling: bool, - interpolation: F.InterpolationMode | None, - do_center_crop: bool, - crop_size: SizeDict, - do_rescale: bool, - rescale_factor: float, - do_normalize: bool, - image_mean: float | list[float] | None, - image_std: float | list[float] | None, - do_pad: bool, - return_tensors: str | TensorType | None, - pad_size: SizeDict | None = None, # Added for transformers >=4.53.0 compatibility - disable_grouping: bool | None = None, # Added for transformers >=4.53.0 compatibility - ) -> BatchFeature: - processed_images = [] - image_sizes = [] - # Determine the size tuple - if size and size.height and size.width: - size_tuple = (size.height, size.width) - else: - size_tuple = (size.shortest_edge, size.shortest_edge) - - # Determine the patch size - if crop_size and crop_size.height: - tile_size = crop_size.height - elif size and size.height: - tile_size = size.height - else: - tile_size = size.shortest_edge - - for image in images: - image_patches = self._get_image_patches( - image, - min_num=min_dynamic_tiles, - max_num=max_dynamic_tiles, - size=size_tuple, - tile_size=tile_size, - use_thumbnail=use_thumbnail, - interpolation=interpolation, - pad_during_tiling=pad_during_tiling, - ) - - # Group images by size for batched processing - processed_image_patches_grouped = {} - # Added for transformers >=4.53.0 compatibility - grouped_image_patches, grouped_image_patches_index = group_images_by_shape( - image_patches, - disable_grouping=disable_grouping, - ) - - for shape, stacked_image_patches in grouped_image_patches.items(): - if do_resize: - stacked_image_patches = self.resize( - image=stacked_image_patches, - size=size, - interpolation=interpolation, - ) - if do_center_crop: - stacked_image_patches = self.center_crop(stacked_image_patches, crop_size) - # Fused rescale and normalize - stacked_image_patches = self.rescale_and_normalize( - stacked_image_patches, - do_rescale, - rescale_factor, - do_normalize, - image_mean, - image_std, - ) - processed_image_patches_grouped[shape] = stacked_image_patches - processed_image_patches = reorder_images( - processed_image_patches_grouped, grouped_image_patches_index - ) - processed_image_patches = ( - torch.stack(processed_image_patches, dim=0) if return_tensors else processed_image_patches - ) - processed_images.append(processed_image_patches) - image_sizes.append(get_image_size(image, ChannelDimension.FIRST)) - - if do_pad: - processed_images = self._pad_for_batching(processed_images) - - # processed_images = torch.stack(processed_images, dim=0) if return_tensors else processed_images - processed_images = torch.cat(processed_images, dim=0) if return_tensors else processed_images - return BatchFeature( - data={"pixel_values": processed_images, "image_sizes": image_sizes}, - tensor_type=return_tensors, - ) - - def preprocess( - self, - images: ImageInput, - videos: VideoInput = None, - **kwargs: Unpack[Eagle25VLFastImageProcessorKwargs], - ) -> BatchFeature: - validate_kwargs( - captured_kwargs=kwargs.keys(), - valid_processor_keys=self.valid_kwargs.__annotations__.keys(), - ) - # Set default kwargs from self. This ensures that if a kwarg is not provided - # by the user, it gets its default value from the instance, or is set to None. - for kwarg_name in self.valid_kwargs.__annotations__: - kwargs.setdefault(kwarg_name, getattr(self, kwarg_name, None)) - - # Extract parameters that are only used for preparing the input images - do_convert_rgb = kwargs.pop("do_convert_rgb") - input_data_format = kwargs.pop("input_data_format") - device = kwargs.pop("device") - # Prepare input images - # transformers >= 4.53.0: uses _prepare_image_like_inputs instead of _prepare_input_images - if images is not None: - images = self._prepare_image_like_inputs( - images=images, - do_convert_rgb=do_convert_rgb, - input_data_format=input_data_format, - device=device, - ) - - if videos is not None: - videos = self._prepare_image_like_inputs( - images=videos, - do_convert_rgb=do_convert_rgb, - input_data_format=input_data_format, - device=device, - ) - - # Update kwargs that need further processing before being validated - kwargs = self._further_process_kwargs(**kwargs) - - # Validate kwargs - self._validate_preprocess_kwargs(**kwargs) - - # torch resize uses interpolation instead of resample - # Added for transformers >=4.53.0 compatibility - resample = kwargs.pop("resample", self.resample) - kwargs["interpolation"] = ( - pil_torch_interpolation_mapping[resample] - if isinstance(resample, PILImageResampling | int) - else resample - ) - - # Filter kwargs to only include those accepted by _preprocess - valid_preprocess_kwargs = { - "do_resize", - "size", - "max_dynamic_tiles", - "min_dynamic_tiles", - "use_thumbnail", - "pad_during_tiling", - "interpolation", - "do_center_crop", - "crop_size", - "do_rescale", - "rescale_factor", - "do_normalize", - "image_mean", - "image_std", - "do_pad", - "return_tensors", - "pad_size", - "disable_grouping", - } - filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_preprocess_kwargs} - if images is not None: - return self._preprocess(images, **filtered_kwargs) - elif videos is not None: - return self._preprocess(videos, **filtered_kwargs) - - -__all__ = ["Eagle25VLImageProcessorFast"] diff --git a/src/lerobot/policies/groot/eagle2_hg_model/modeling_eagle2_5_vl.py b/src/lerobot/policies/groot/eagle2_hg_model/modeling_eagle2_5_vl.py deleted file mode 100755 index 6e5532ea4..000000000 --- a/src/lerobot/policies/groot/eagle2_hg_model/modeling_eagle2_5_vl.py +++ /dev/null @@ -1,396 +0,0 @@ -# -------------------------------------------------------- -# NVIDIA -# Copyright (c) 2025 NVIDIA -# Licensed under The MIT License [see LICENSE for details] -# -------------------------------------------------------- - -import inspect - -import torch -import torch.utils.checkpoint as cp -from peft import LoraConfig, get_peft_model -from torch import nn -from torch.nn import CrossEntropyLoss -from transformers import GenerationConfig -from transformers.generation import GenerationMixin -from transformers.modeling_outputs import CausalLMOutputWithPast -from transformers.modeling_utils import PreTrainedModel -from transformers.models.llama.modeling_llama import LlamaForCausalLM -from transformers.models.qwen2.modeling_qwen2 import Qwen2ForCausalLM -from transformers.models.qwen3.modeling_qwen3 import Qwen3ForCausalLM -from transformers.models.siglip.modeling_siglip import SiglipVisionModel -from transformers.utils import add_start_docstrings, logging - -from .configuration_eagle2_5_vl import Eagle25VLConfig - -logger = logging.get_logger(__name__) - - -# copy from https://github.com/huggingface/transformers/blob/main/src/transformers/models/llava_onevision/modeling_llava_onevision.py#L241C1-L280C1 -EAGLE2_5_VL_START_DOCSTRING = r""" - This model inherits from [`PreTrainedModel`]. Check the superclass documentation for the generic methods the - library implements for all its model (such as downloading or saving, resizing the input embeddings, pruning heads - etc.) - - This model is also a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass. - Use it as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage - and behavior. - - Parameters: - config ([`Eagle25VLConfig`]): - Model configuration class with all the parameters of the model. Initializing with a config file does not - load the weights associated with the model, only the configuration. Check out the - [`~PreTrainedModel.from_pretrained`] method to load the model weights. -""" - - -@add_start_docstrings( - "The bare Eagle2_5_VL Model outputting raw hidden-states without any specific head on top.", - EAGLE2_5_VL_START_DOCSTRING, -) -class Eagle25VLPreTrainedModel(PreTrainedModel): - config_class = Eagle25VLConfig - base_model_prefix = "model" - main_input_name = "input_ids" - supports_gradient_checkpointing = True - _no_split_modules = [ - "Qwen2DecoderLayer", - "LlamaDecoderLayer", - "Siglip2EncoderLayer", - "SiglipEncoderLayer", - ] - _skip_keys_device_placement = "past_key_values" - _supports_flash_attn = True - _supports_flash_attn_2 = True - _supports_cache_class = True - _supports_static_cache = True - _supports_quantized_cache = True - _supports_sdpa = True - - def _init_weights(self, module): - std = self.config.initializer_range - if isinstance(module, nn.Linear | nn.Conv2d): - module.weight.data.normal_(mean=0.0, std=std) - if module.bias is not None: - module.bias.data.zero_() - elif isinstance(module, nn.Embedding): - module.weight.data.normal_(mean=0.0, std=std) - if module.padding_idx is not None: - module.weight.data[module.padding_idx].zero_() - - -class Eagle25VLForConditionalGeneration(Eagle25VLPreTrainedModel, GenerationMixin): - config_class = Eagle25VLConfig - - def __init__(self, config: Eagle25VLConfig, vision_model=None, language_model=None): - super().__init__(config) - - image_size = config.force_image_size or config.vision_config.image_size - patch_size = config.vision_config.patch_size - self.patch_size = patch_size - if config.use_pixel_shuffle: - self.num_image_token = int((image_size // patch_size) ** 2 * (config.downsample_ratio**2)) - else: - self.num_image_token = int((image_size // patch_size) ** 2) - - self.select_layer = config.select_layer - self.downsample_ratio = config.downsample_ratio - self.loss_version = config.loss_version - self.mlp_checkpoint = config.mlp_checkpoint - self.use_pixel_shuffle = config.use_pixel_shuffle - self.mlp_connector_layers = config.mlp_connector_layers - logger.info(f"num_image_token: {self.num_image_token}") - logger.info(f"mlp_checkpoint: {self.mlp_checkpoint}") - if vision_model is not None: - self.vision_model = vision_model - else: - if config.vision_config.model_type == "siglip_vision_model": - config.vision_config._attn_implementation = "flash_attention_2" - self.vision_model = SiglipVisionModel(config.vision_config) - else: - raise NotImplementedError(f"{config.vision_config.model_type} is not implemented.") - - if language_model is not None: - self.language_model = language_model - else: - if config.text_config.architectures[0] == "LlamaForCausalLM": - self.language_model = LlamaForCausalLM(config.text_config) - elif config.text_config.architectures[0] == "Phi3ForCausalLM": - raise NotImplementedError("Phi3 is not implemented.") - # self.language_model = Phi3ForCausalLM(config.text_config) - elif config.text_config.architectures[0] == "Qwen2ForCausalLM": - assert config.text_config._attn_implementation == "flash_attention_2", ( - f"Qwen2 must use flash_attention_2 but got {config.text_config._attn_implementation}" - ) - self.language_model = Qwen2ForCausalLM(config.text_config) - elif config.text_config.architectures[0] == "Qwen3ForCausalLM": - self.language_model = Qwen3ForCausalLM(config.text_config) - else: - raise NotImplementedError(f"{config.text_config.architectures[0]} is not implemented.") - - vit_hidden_size = config.vision_config.hidden_size - llm_hidden_size = config.text_config.hidden_size - - if config.mlp_connector_layers == 2: - self.mlp1 = nn.Sequential( - nn.LayerNorm(vit_hidden_size * int(1 / self.downsample_ratio) ** 2), - nn.Linear(vit_hidden_size * int(1 / self.downsample_ratio) ** 2, llm_hidden_size), - nn.GELU(), - nn.Linear(llm_hidden_size, llm_hidden_size), - ) - elif config.mlp_connector_layers == 1 and config.use_pixel_shuffle: - self.mlp1 = nn.Sequential( - nn.Linear(vit_hidden_size * int(1 / self.downsample_ratio) ** 2, llm_hidden_size), - ) - elif config.mlp_connector_layers == 1 and not config.use_pixel_shuffle: - self.mlp1 = nn.Sequential( - nn.Linear(vit_hidden_size, llm_hidden_size), - ) - else: - raise NotImplementedError(f"{config.mlp_connector_layers} is not implemented.") - - self.image_token_index = config.image_token_index - self.neftune_alpha = None - - if config.use_backbone_lora: - self.wrap_backbone_lora(r=config.use_backbone_lora, lora_alpha=2 * config.use_backbone_lora) - - self.use_llm_lora = config.use_llm_lora - if config.use_llm_lora: - self.wrap_llm_lora(r=config.use_llm_lora, lora_alpha=2 * config.use_llm_lora) - - self.check_forward_kwargs() - - def check_forward_kwargs(self): - # We intentionally avoid using **kwargs in forward because Hugging Face Transformers - # has special handling for functions with **kwargs parameters that would affect - # how our model is processed during training and inference. - forward_params = inspect.signature(self.forward).parameters - assert not any(k.kind == inspect.Parameter.VAR_KEYWORD for k in forward_params.values()) - - def wrap_backbone_lora(self, r=128, lora_alpha=256, lora_dropout=0.05): - lora_config = LoraConfig( - r=r, - target_modules=[ - "self_attn.q_proj", - "self_attn.k_proj", - "self_attn.v_proj", - "self_attn.out_proj", - "mlp.fc1", - "mlp.fc2", - ], - lora_alpha=lora_alpha, - lora_dropout=lora_dropout, - ) - self.vision_model = get_peft_model(self.vision_model, lora_config) - self.vision_model.print_trainable_parameters() - - def wrap_llm_lora(self, r=128, lora_alpha=256, lora_dropout=0.05): - lora_config = LoraConfig( - r=r, - target_modules=[ - "self_attn.q_proj", - "self_attn.k_proj", - "self_attn.v_proj", - "self_attn.o_proj", - "mlp.gate_proj", - "mlp.down_proj", - "mlp.up_proj", - ], - lora_alpha=lora_alpha, - lora_dropout=lora_dropout, - task_type="CAUSAL_LM", - ) - self.language_model = get_peft_model(self.language_model, lora_config) - self.language_model.enable_input_require_grads() - self.language_model.print_trainable_parameters() - self.use_llm_lora = True - - def forward( - self, - pixel_values: torch.FloatTensor, - input_ids: torch.LongTensor = None, - attention_mask: torch.Tensor | None = None, - position_ids: torch.LongTensor | None = None, - image_flags: torch.LongTensor | None = None, - past_key_values: list[torch.FloatTensor] | None = None, - labels: torch.LongTensor | None = None, - use_cache: bool | None = None, - output_attentions: bool | None = None, - output_hidden_states: bool | None = None, - return_dict: bool | None = None, - num_tiles_list: list[torch.Tensor] | None = None, - ) -> tuple | CausalLMOutputWithPast: - return_dict = return_dict if return_dict is not None else self.config.use_return_dict - - input_embeds = self.language_model.get_input_embeddings()(input_ids) - - vit_embeds = self.extract_feature(pixel_values) - - if image_flags is not None: - image_flags = image_flags.view(-1) - vit_embeds = vit_embeds[image_flags == 1] - - b, n, c = input_embeds.shape - input_embeds = input_embeds.reshape(b * n, c) - - input_ids = input_ids.reshape(b * n) - selected = input_ids == self.image_token_index - try: - input_embeds[selected] = input_embeds[selected] * 0.0 + vit_embeds.reshape(-1, c) - except Exception as e: - vit_embeds = vit_embeds.reshape(-1, c) - print( - f"warning: {e}, input_embeds[selected].shape={input_embeds[selected].shape}, " - f"vit_embeds.shape={vit_embeds.shape}" - ) - n_token = selected.sum() - input_embeds[selected] = input_embeds[selected] * 0.0 + vit_embeds[:n_token] - - input_embeds = input_embeds.reshape(b, n, c) - - outputs = self.language_model( - inputs_embeds=input_embeds, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - ) - logits = outputs.logits - - loss = None - if labels is not None: - # Shift so that tokens < n predict n - shift_logits = logits[..., :-1, :].contiguous() - shift_labels = labels[..., 1:].contiguous() - # Flatten the tokens - loss_fct = CrossEntropyLoss() - shift_logits = shift_logits.view(-1, self.language_model.config.vocab_size) - shift_labels = shift_labels.view(-1) - # Enable model parallelism - shift_labels = shift_labels.to(shift_logits.device) - loss = loss_fct(shift_logits, shift_labels) - - if not return_dict: - output = (logits,) + outputs[1:] - return (loss,) + output if loss is not None else output - - return CausalLMOutputWithPast( - loss=loss, - logits=logits, - past_key_values=outputs.past_key_values, - hidden_states=outputs.hidden_states, - attentions=outputs.attentions, - ) - - def pixel_shuffle(self, x, scale_factor=0.5): - n, w, h, c = x.size() - # N, W, H, C --> N, W, H * scale, C // scale - x = x.view(n, w, int(h * scale_factor), int(c / scale_factor)) - # N, W, H * scale, C // scale --> N, H * scale, W, C // scale - x = x.permute(0, 2, 1, 3).contiguous() - # N, H * scale, W, C // scale --> N, H * scale, W * scale, C // (scale ** 2) - x = x.view(n, int(h * scale_factor), int(w * scale_factor), int(c / (scale_factor * scale_factor))) - - x = x.permute(0, 2, 1, 3).contiguous() - return x - - def extract_feature(self, pixel_values): - if self.select_layer == -1: - vit_embeds = self.vision_model( - pixel_values=pixel_values, output_hidden_states=False, return_dict=True - ) - if hasattr(vit_embeds, "last_hidden_state"): - vit_embeds = vit_embeds.last_hidden_state - - else: - vit_embeds = self.vision_model( - pixel_values=pixel_values, output_hidden_states=True, return_dict=True - ).hidden_states[self.select_layer] - - if self.use_pixel_shuffle: - h = w = int(vit_embeds.shape[1] ** 0.5) - vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], h, w, -1) - vit_embeds = self.pixel_shuffle( - vit_embeds, scale_factor=self.downsample_ratio - ) # torch.Size([B, 1024, 1024]) -> torch.Size([B, 16, 16, 4096]) - vit_embeds = vit_embeds.reshape( - vit_embeds.shape[0], -1, vit_embeds.shape[-1] - ) # torch.Size([B, 16, 16, 4096]) -> torch.Size([B, 256, 4096]) - - if self.mlp_checkpoint and vit_embeds.requires_grad: - vit_embeds = cp.checkpoint(self.mlp1, vit_embeds) - else: - vit_embeds = self.mlp1(vit_embeds) - - return vit_embeds - - @torch.no_grad() - def generate( - self, - pixel_values: torch.FloatTensor | None = None, - input_ids: torch.FloatTensor | None = None, - attention_mask: torch.LongTensor | None = None, - visual_features: torch.FloatTensor | None = None, - generation_config: GenerationConfig | None = None, - output_hidden_states: bool | None = None, - image_sizes: list[tuple[int, int]] | None = None, - **generate_kwargs, - ) -> torch.LongTensor: - if pixel_values is not None: - if visual_features is not None: - vit_embeds = visual_features - else: - vit_embeds = self.extract_feature(pixel_values) - - input_embeds = self.language_model.get_input_embeddings()(input_ids) - b, n, c = input_embeds.shape - input_embeds = input_embeds.reshape(b * n, c) - - input_ids = input_ids.reshape(b * n) - selected = input_ids == self.config.image_token_index - assert selected.sum() != 0 - input_embeds[selected] = vit_embeds.reshape(-1, c).to(input_embeds.device) - - input_embeds = input_embeds.reshape(b, n, c) - else: - input_embeds = self.language_model.get_input_embeddings()(input_ids) - - if "use_cache" not in generate_kwargs: - generate_kwargs["use_cache"] = True - - outputs = self.language_model.generate( - inputs_embeds=input_embeds, - attention_mask=attention_mask, - generation_config=generation_config, - output_hidden_states=output_hidden_states, - **generate_kwargs, - ) - - return outputs - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.get_input_embeddings - def get_input_embeddings(self): - return self.language_model.get_input_embeddings() - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.set_input_embeddings - def set_input_embeddings(self, value): - self.language_model.set_input_embeddings(value) - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.get_output_embeddings - def get_output_embeddings(self): - return self.language_model.get_output_embeddings() - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.set_output_embeddings - def set_output_embeddings(self, new_embeddings): - self.language_model.set_output_embeddings(new_embeddings) - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.set_decoder - def set_decoder(self, decoder): - self.language_model.set_decoder(decoder) - - # Copied from transformers.models.llava_next.modeling_llava_next.LlavaNextForConditionalGeneration.get_decoder - def get_decoder(self): - return self.language_model.get_decoder() diff --git a/src/lerobot/policies/groot/eagle2_hg_model/processing_eagle2_5_vl.py b/src/lerobot/policies/groot/eagle2_hg_model/processing_eagle2_5_vl.py deleted file mode 100755 index b36e70c47..000000000 --- a/src/lerobot/policies/groot/eagle2_hg_model/processing_eagle2_5_vl.py +++ /dev/null @@ -1,541 +0,0 @@ -# Copyright 2024 The HuggingFace Inc. team. -# -# 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. -""" -Processor class for Eagle25VL. -copy from https://github.com/huggingface/transformers/blob/main/src/transformers/models/llava_onevision/processing_llava_onevision.py -""" - -import base64 -import os -import re -from io import BytesIO - -import requests -import torch -from PIL import Image -from transformers.feature_extraction_utils import BatchFeature -from transformers.image_utils import ImageInput -from transformers.processing_utils import ProcessingKwargs, ProcessorMixin, Unpack -from transformers.tokenization_utils_base import PreTokenizedInput, TextInput -from transformers.utils import logging -from transformers.video_utils import VideoInput - -logger = logging.get_logger(__name__) - - -FRAME_FACTOR = 2 -FPS = 2.0 -FPS_MIN_FRAMES = 4 -FPS_MAX_FRAMES = 256 - - -def to_rgb(pil_image: Image.Image) -> Image.Image: - if pil_image.mode == "RGBA": - white_background = Image.new("RGB", pil_image.size, (255, 255, 255)) - white_background.paste(pil_image, mask=pil_image.split()[3]) # Use alpha channel as mask - return white_background - else: - return pil_image.convert("RGB") - - -def fetch_image(ele: dict[str, str | Image.Image]) -> Image.Image: - image = ele["image"] if "image" in ele else ele["image_url"] - image_obj = None - if isinstance(image, Image.Image): - image_obj = image - elif image.startswith("http://") or image.startswith("https://"): - response = requests.get(image, stream=True, timeout=10) - image_obj = Image.open(BytesIO(response.content)) - elif image.startswith("file://"): - image_obj = Image.open(image[7:]) - elif image.startswith("data:image"): - if "base64," in image: - _, base64_data = image.split("base64,", 1) - data = base64.b64decode(base64_data) - image_obj = Image.open(BytesIO(data)) - else: - image_obj = Image.open(image) - if image_obj is None: - raise ValueError( - f"Unrecognized image input, support local path, http url, base64 and PIL.Image, got {image}" - ) - image = to_rgb(image_obj) - if "scale_factor" in ele: - scale_factor = ele["scale_factor"] - image = image.resize((image.width * scale_factor, image.height * scale_factor), Image.BILINEAR) - return image - - -class Eagle25VLProcessorKwargs(ProcessingKwargs, total=False): - # see processing_utils.ProcessingKwargs documentation for usage. - _defaults = { - "text_kwargs": { - "padding": False, - }, - "images_kwargs": {}, - "videos_kwargs": {"max_dynamic_tiles": 1}, - } - - -class Eagle25VLProcessor(ProcessorMixin): - r""" - Constructs a Eagle25VL processor which wraps a Eagle25VL video processor, Eagle25VL image processor and a Eagle25VL tokenizer into a single processor. - - [`Eagle25VLProcessor`] offers all the functionalities of [`Eagle25VLVideoProcessor`], [`Eagle25VLImageProcessor`] and [`Eagle25VLTokenizer`]. See the - [`~Eagle25VLVideoProcessor.__call__`], [`~Eagle25VLProcessor.__call__`] and [`~Eagle25VLProcessor.decode`] for more information. - - Args: - image_processor ([`LlavaOnevisionImageProcessor`], *optional*): - The image processor is a required input. - tokenizer ([`LlamaTokenizerFast`], *optional*): - The tokenizer is a required input. - num_image_tokens (`int`, *optional*): - Number of image tokens for one imagethat will be returned by vision tower. - vision_feature_select_strategy (`str`, *optional*): - The feature selection strategy used to select the vision feature from the vision backbone. - Should be same as in model's config - chat_template (`str`, *optional*): A Jinja template which will be used to convert lists of messages - in a chat into a tokenizable string. - image_token (`str`, *optional*, defaults to `""`): - Special token used to denote image location. - video_token (`str`, *optional*, defaults to `"