mirror of
https://github.com/huggingface/lerobot.git
synced 2026-08-08 17:39:44 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aaeae12f8f | |||
| 9f1b520796 | |||
| 6c73c413eb | |||
| 3aabd135d3 | |||
| 2c1adc378e | |||
| 266be2bd17 | |||
| ff7cc3de1d | |||
| 31fedfd9dd | |||
| b1bf24f565 |
@@ -33,7 +33,7 @@ jobs:
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository == 'huggingface/lerobot'
|
||||
uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||
uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@931031bf2b54aabb134ceb54980a6a2860a00f11 # main
|
||||
with:
|
||||
package_name: lerobot
|
||||
secrets:
|
||||
|
||||
@@ -60,10 +60,14 @@ jobs:
|
||||
github.repository == 'huggingface/lerobot'
|
||||
permissions:
|
||||
contents: read
|
||||
uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||
uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@931031bf2b54aabb134ceb54980a6a2860a00f11 # main
|
||||
with:
|
||||
commit_sha: ${{ github.sha }}
|
||||
package: lerobot
|
||||
# The shared workflow builds its venv with the runner's system Python, which is 3.10 on
|
||||
# ubuntu-22.04. lerobot requires >=3.12, so without this the install fails during setup —
|
||||
# before `pre_command` below ever runs. Added upstream in huggingface/doc-builder#808.
|
||||
python_version: "3.12"
|
||||
# doc-builder ships a mock-deps registry entry for lerobot, so the reusable workflow takes its
|
||||
# "light install" path: `pip install ./lerobot --no-deps` plus a handful of real dependencies.
|
||||
# That is not enough to import lerobot — draccus runs `register_subclass` at import time and
|
||||
@@ -92,11 +96,12 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||
uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@931031bf2b54aabb134ceb54980a6a2860a00f11 # main
|
||||
with:
|
||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||
pr_number: ${{ github.event.number }}
|
||||
package: lerobot
|
||||
# See the comment on build_main_docs. The PR workflow passes its own `--version pr_<n>`, so no
|
||||
# additional_args are needed here.
|
||||
python_version: "3.12"
|
||||
pre_command: uv pip install "./lerobot[dataset]"
|
||||
|
||||
@@ -9,19 +9,7 @@ Each policy has its own guide with training recipes and results — [ACT](../act
|
||||
|
||||
## PreTrainedPolicy
|
||||
|
||||
The abstract base class every policy subclasses. `forward` computes the training loss, `select_action`
|
||||
returns one action at a time for control loops, and `predict_action_chunk` returns a full action chunk.
|
||||
|
||||
[[autodoc]] lerobot.policies.pretrained.PreTrainedPolicy
|
||||
- forward
|
||||
- predict_action_chunk
|
||||
- select_action
|
||||
- get_optim_params
|
||||
- reset
|
||||
- from_pretrained
|
||||
- supports_rtc
|
||||
- push_model_to_hub
|
||||
- wrap_with_peft
|
||||
|
||||
## PreTrainedConfig
|
||||
|
||||
@@ -30,148 +18,3 @@ returns one action at a time for control loops, and `predict_action_chunk` retur
|
||||
## make_policy
|
||||
|
||||
[[autodoc]] lerobot.policies.factory.make_policy
|
||||
|
||||
## get_policy_class
|
||||
|
||||
[[autodoc]] lerobot.policies.factory.get_policy_class
|
||||
|
||||
## make_policy_config
|
||||
|
||||
[[autodoc]] lerobot.policies.factory.make_policy_config
|
||||
|
||||
## make_pre_post_processors
|
||||
|
||||
[[autodoc]] lerobot.policies.factory.make_pre_post_processors
|
||||
|
||||
## ACT
|
||||
|
||||
[[autodoc]] lerobot.policies.act.modeling_act.ACTPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.act.configuration_act.ACTConfig
|
||||
|
||||
## SmolVLA
|
||||
|
||||
[[autodoc]] lerobot.policies.smolvla.modeling_smolvla.SmolVLAPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.smolvla.configuration_smolvla.SmolVLAConfig
|
||||
|
||||
## π₀ (PI0)
|
||||
|
||||
[[autodoc]] lerobot.policies.pi0.modeling_pi0.PI0Policy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.pi0.configuration_pi0.PI0Config
|
||||
|
||||
## π₀-FAST (PI0Fast)
|
||||
|
||||
[[autodoc]] lerobot.policies.pi0_fast.modeling_pi0_fast.PI0FastPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.pi0_fast.configuration_pi0_fast.PI0FastConfig
|
||||
|
||||
## π₀.₅ (PI05)
|
||||
|
||||
[[autodoc]] lerobot.policies.pi05.modeling_pi05.PI05Policy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.pi05.configuration_pi05.PI05Config
|
||||
|
||||
## MolmoAct2
|
||||
|
||||
[[autodoc]] lerobot.policies.molmoact2.modeling_molmoact2.MolmoAct2Policy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.molmoact2.configuration_molmoact2.MolmoAct2Config
|
||||
|
||||
## VLA-JEPA
|
||||
|
||||
[[autodoc]] lerobot.policies.vla_jepa.modeling_vla_jepa.VLAJEPAPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.vla_jepa.configuration_vla_jepa.VLAJEPAConfig
|
||||
|
||||
## EO-1
|
||||
|
||||
[[autodoc]] lerobot.policies.eo1.modeling_eo1.EO1Policy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.eo1.configuration_eo1.EO1Config
|
||||
|
||||
## LingBot-VA
|
||||
|
||||
[[autodoc]] lerobot.policies.lingbot_va.modeling_lingbot_va.LingBotVAPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.lingbot_va.configuration_lingbot_va.LingBotVAConfig
|
||||
|
||||
## FastWAM
|
||||
|
||||
[[autodoc]] lerobot.policies.fastwam.modeling_fastwam.FastWAMPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.fastwam.configuration_fastwam.FastWAMConfig
|
||||
|
||||
## EVO1
|
||||
|
||||
[[autodoc]] lerobot.policies.evo1.modeling_evo1.Evo1Policy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.evo1.configuration_evo1.Evo1Config
|
||||
|
||||
## NVIDIA GR00T
|
||||
|
||||
[[autodoc]] lerobot.policies.groot.modeling_groot.GrootPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.groot.configuration_groot.GrootConfig
|
||||
|
||||
## X-VLA
|
||||
|
||||
[[autodoc]] lerobot.policies.xvla.modeling_xvla.XVLAPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.xvla.configuration_xvla.XVLAConfig
|
||||
|
||||
## Multitask DiT Policy
|
||||
|
||||
[[autodoc]] lerobot.policies.multi_task_dit.modeling_multi_task_dit.MultiTaskDiTPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.multi_task_dit.configuration_multi_task_dit.MultiTaskDiTConfig
|
||||
|
||||
## WALL-OSS
|
||||
|
||||
[[autodoc]] lerobot.policies.wall_x.modeling_wall_x.WallXPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.wall_x.configuration_wall_x.WallXConfig
|
||||
|
||||
## Diffusion Policy
|
||||
|
||||
[[autodoc]] lerobot.policies.diffusion.modeling_diffusion.DiffusionPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.diffusion.configuration_diffusion.DiffusionConfig
|
||||
|
||||
## Gaussian Actor
|
||||
|
||||
[[autodoc]] lerobot.policies.gaussian_actor.modeling_gaussian_actor.GaussianActorPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.gaussian_actor.configuration_gaussian_actor.GaussianActorConfig
|
||||
|
||||
## TD-MPC
|
||||
|
||||
[[autodoc]] lerobot.policies.tdmpc.modeling_tdmpc.TDMPCPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.tdmpc.configuration_tdmpc.TDMPCConfig
|
||||
|
||||
## VQ-BeT
|
||||
|
||||
[[autodoc]] lerobot.policies.vqbet.modeling_vqbet.VQBeTPolicy
|
||||
- all
|
||||
|
||||
[[autodoc]] lerobot.policies.vqbet.configuration_vqbet.VQBeTConfig
|
||||
|
||||
@@ -62,7 +62,10 @@ Reference data points on a 4×H100 80 GB cluster (`accelerate launch --num_proce
|
||||
| `smolvla` | 27m 49s | 0.312 | 0.011 | ~80% | `--policy.path=lerobot/smolvla_base`, `freeze_vision_encoder=false`, `train_expert_only=false` |
|
||||
| `pi05` | 3h 41m | 2.548 | 0.014 | ~95% | `--policy.pretrained_path=lerobot/pi05_base`, `gradient_checkpointing=true`, `dtype=bfloat16`, vision encoder + expert trained |
|
||||
|
||||
The `dataloading_s` vs. `update_s` ratio is the diagnostic that matters: when `dataloading_s` approaches `update_s`, more GPUs stop helping — your dataloader is the bottleneck and you should look at `--num_workers`, image resolution, and disk speed before adding compute.
|
||||
Training logs separate the full iteration into `dataloading_s` (`next(dl_iter)`), `preprocessing_s`
|
||||
(image conversion and the policy pipeline), and `update_s` (the optimizer update). `step_s` covers all
|
||||
three and drives `samples_per_s`. The benchmark above predates this split, so its `dataloading_s` includes
|
||||
preprocessing.
|
||||
|
||||
### Schedule and checkpoints
|
||||
|
||||
|
||||
@@ -242,6 +242,17 @@ python src/lerobot/scripts/augment_dataset_quantile_stats.py \
|
||||
--repo-id=your_dataset
|
||||
```
|
||||
|
||||
Recording, resuming, and merging aggregate quantiles from per-episode summaries, so `meta/stats.json` ends up holding a conservative envelope (`min` for `q <= 50`, `max` for `q > 50`) rather than whole-dataset quantiles. To estimate the latter, scan every episode with a running histogram:
|
||||
|
||||
```bash
|
||||
python src/lerobot/scripts/augment_dataset_quantile_stats.py \
|
||||
--repo-id=your_dataset \
|
||||
--overwrite \
|
||||
--skip-images
|
||||
```
|
||||
|
||||
`--skip-images` keeps the existing image statistics and avoids video decoding when only `STATE`/`ACTION` need recomputing, and `--root` reads a local dataset instead of the Hub. These values are histogram estimates, subject to discretization and rebinning error, so they can differ from the conservative ones — which changes MolmoAct2's normalized targets and therefore its loss scale. Statistics already saved inside an existing checkpoint are not affected.
|
||||
|
||||
Alternatively, train MolmoAct2 with mean/std normalization:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -127,6 +127,17 @@ lerobot-edit-dataset \
|
||||
|
||||
Or keep the dataset as-is and pass `--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}'`.
|
||||
|
||||
Recording, resuming, and merging aggregate quantiles from per-episode summaries, so `meta/stats.json` ends up holding a conservative envelope (`min` for `q <= 50`, `max` for `q > 50`) rather than whole-dataset quantiles. To estimate the latter, scan every episode with a running histogram:
|
||||
|
||||
```bash
|
||||
python src/lerobot/scripts/augment_dataset_quantile_stats.py \
|
||||
--repo-id=your_dataset \
|
||||
--overwrite \
|
||||
--skip-images
|
||||
```
|
||||
|
||||
`--skip-images` keeps the existing image statistics and avoids video decoding when only `STATE`/`ACTION` need recomputing, and `--root` reads a local dataset instead of the Hub. These values are histogram estimates, subject to discretization and rebinning error, so they can differ from the conservative ones — which changes π₀.₅'s normalized targets and therefore its loss scale. Statistics already saved inside an existing checkpoint are not affected.
|
||||
|
||||
### Training Command Example
|
||||
|
||||
The same finetune with the VLM frozen: less memory, at some cost in success rate. Swap `--dataset.repo_id` for your own dataset.
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
https://diffusion-policy.cs.columbia.edu
|
||||
|
||||
## Training
|
||||
|
||||
The reference implementation maintains an exponential moving average (EMA) of the policy weights during training and evaluates the EMA weights. To reproduce this behavior, enable the trainer's EMA shadow:
|
||||
|
||||
```bash
|
||||
lerobot-train \
|
||||
--policy.type=diffusion \
|
||||
--ema.enable=true \
|
||||
...
|
||||
```
|
||||
|
||||
Checkpoints then contain a directly loadable copy of the EMA weights next to the live ones, e.g. for evaluation:
|
||||
|
||||
```bash
|
||||
lerobot-eval --policy.path=outputs/train/.../checkpoints/last/pretrained_model_ema ...
|
||||
```
|
||||
|
||||
The EMA decay schedule (`--ema.inv_gamma`, `--ema.power`, ...) defaults to the reference implementation's values. For a constant decay instead of the warmup schedule (e.g. to match openpi's pi0/pi05 training), set `--ema.decay=0.99`.
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
|
||||
@@ -59,6 +59,22 @@ When `use_relative_actions=true`, the training script automatically:
|
||||
|
||||
---
|
||||
|
||||
## EMA of the policy weights
|
||||
|
||||
OpenPI maintains an exponential moving average of the weights during training (`ema_decay=0.99` by default) and keeps the EMA copy for inference. To reproduce this with the LeRobot trainer, enable the EMA shadow with a constant decay:
|
||||
|
||||
```bash
|
||||
python -m lerobot.scripts.lerobot_train \
|
||||
--policy.type=pi05 \
|
||||
--dataset.repo_id=your_org/your_dataset \
|
||||
--ema.enable=true \
|
||||
--ema.decay=0.99
|
||||
```
|
||||
|
||||
Checkpoints then contain a directly loadable copy of the EMA weights in `pretrained_model_ema/` next to the live ones. Note that the shadow is a full extra copy of the parameters on the GPU. Like OpenPI (which disables EMA in its LoRA configs), EMA is not supported together with PEFT adapters.
|
||||
|
||||
---
|
||||
|
||||
## Citation
|
||||
|
||||
If you use this work, please cite both **OpenPI** and the π₀.₅ paper:
|
||||
|
||||
@@ -26,7 +26,7 @@ def send_action(self, action: RobotAction, rate_hz: float = 30.0) -> RobotAction
|
||||
action (`dict[str, float]`):
|
||||
Target values keyed by motor name, e.g. `{"shoulder_pan.pos": 0.0}`. Keys must match the
|
||||
robot's action features.
|
||||
rate_hz (`float`, *optional*, defaults to `30.0`):
|
||||
rate_hz (`float`, *optional*, defaults to 30.0):
|
||||
Control loop frequency.
|
||||
|
||||
Returns:
|
||||
@@ -60,7 +60,7 @@ description, then the sections.
|
||||
### The `Args:` line is machine-parsed
|
||||
|
||||
```
|
||||
name (`type`, *optional*, defaults to `X`):
|
||||
name (`type`, *optional*, defaults to X):
|
||||
Description, indented on its own line.
|
||||
```
|
||||
|
||||
@@ -81,6 +81,10 @@ fails. Omit the clause entirely for required parameters:
|
||||
Types go in backticks. Use `*optional*` with no `defaults to` when the default is `None` or is otherwise not
|
||||
worth restating.
|
||||
|
||||
The default value itself follows one rule, and the checker rewrites to match it: **numbers are bare,
|
||||
everything else is backticked** — `defaults to 30`, `defaults to 1e-05`, but `` defaults to `True` ``,
|
||||
`` defaults to `"socketcan"` ``. Booleans count as "everything else", not as numbers.
|
||||
|
||||
### `Returns:` is type-first
|
||||
|
||||
One indented line, type first, then a colon, then the description:
|
||||
@@ -179,7 +183,33 @@ Add files containing runnable examples to `utils/documentation_tests.txt`.
|
||||
|
||||
Put examples on the three to five genuine entry points of a module. Examples on trivial accessors are noise.
|
||||
|
||||
## Three patterns you will hit constantly
|
||||
## Four patterns you will hit constantly
|
||||
|
||||
### Constructor parameters go on the class
|
||||
|
||||
**doc-builder renders a class from its class docstring and never reads `__init__.__doc__`.** An `Args:`
|
||||
block written on `__init__` is dropped from the page entirely — the parameter still appears in the rendered
|
||||
signature, but with no description beside it.
|
||||
|
||||
Document constructor parameters in an `Args:` block on the **class** docstring:
|
||||
|
||||
```python
|
||||
class SOFollower(Robot):
|
||||
"""A single SO-family follower arm.
|
||||
|
||||
Args:
|
||||
config (`SOFollowerRobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
def __init__(self, config: SOFollowerRobotConfig):
|
||||
super().__init__(config)
|
||||
```
|
||||
|
||||
`__init__` then needs no docstring at all — `D107` is disabled repo-wide for exactly this reason. The
|
||||
payoff is not only that the parameters render: an `Args:` block on the class is checked against
|
||||
`inspect.signature(cls)` by `make check-docstrings`, so it cannot silently drift from the constructor. The
|
||||
same block on `__init__` is checked by nothing.
|
||||
|
||||
### Config dataclasses
|
||||
|
||||
|
||||
+7
-39
@@ -413,6 +413,11 @@ ignore = [
|
||||
# rendered page. Coverage of the things that do get rendered is enforced by interrogate instead.
|
||||
"D100",
|
||||
"D104",
|
||||
# D107: `__init__` docstrings. doc-builder renders a class from its *class* docstring and never reads
|
||||
# `__init__.__doc__`, so anything documented there is dropped from the page. Constructor parameters
|
||||
# belong in an `Args:` block on the class, where they render and where `make check-docstrings`
|
||||
# validates them against the signature.
|
||||
"D107",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
@@ -448,44 +453,7 @@ ignore = [
|
||||
"src/lerobot/model/**" = ["D"]
|
||||
"src/lerobot/motors/**" = ["D"]
|
||||
"src/lerobot/optim/**" = ["D"]
|
||||
"src/lerobot/policies/__init__.py" = ["D"]
|
||||
"src/lerobot/policies/pi_gemma.py" = ["D"]
|
||||
"src/lerobot/policies/common/**" = ["D"]
|
||||
# Wave 3 of the docstring initiative documents each policy family's config class in full, plus only
|
||||
# the public forward/select_action surface of modeling_*.py's main <Family>Policy class and the
|
||||
# processor_*.py's make_<family>_pre_post_processors factory. modeling_*.py and processor_*.py also
|
||||
# contain internal building blocks (nn.Module helpers, ProcessorStep internals) that remain out of
|
||||
# scope, so those two file patterns stay D-ignored wholesale rather than enumerated per symbol; the
|
||||
# narrower Policy/processor-factory scope is instead enforced via the AST coverage check and
|
||||
# utils/check_docstrings.py's leaf-module entries. configuration_*.py is fully documented and stays
|
||||
# checked here.
|
||||
"src/lerobot/policies/*/modeling_*.py" = ["D"]
|
||||
"src/lerobot/policies/*/processor_*.py" = ["D"]
|
||||
"src/lerobot/policies/evo1/evo1_model.py" = ["D"]
|
||||
"src/lerobot/policies/evo1/flow_matching.py" = ["D"]
|
||||
"src/lerobot/policies/evo1/internvl3_embedder.py" = ["D"]
|
||||
"src/lerobot/policies/fastwam/wan/**" = ["D"]
|
||||
"src/lerobot/policies/groot/action_head/**" = ["D"]
|
||||
"src/lerobot/policies/groot/groot_n1_7.py" = ["D"]
|
||||
"src/lerobot/policies/groot/utils.py" = ["D"]
|
||||
"src/lerobot/policies/lingbot_va/utils.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/action_interpolator.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/action_queue.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/debug_tracker.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/debug_visualizer.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/latency_tracker.py" = ["D"]
|
||||
"src/lerobot/policies/rtc/relative.py" = ["D"]
|
||||
"src/lerobot/policies/smolvla/smolvlm_with_expert.py" = ["D"]
|
||||
"src/lerobot/policies/vla_jepa/action_head.py" = ["D"]
|
||||
"src/lerobot/policies/vla_jepa/qwen_interface.py" = ["D"]
|
||||
"src/lerobot/policies/vla_jepa/world_model.py" = ["D"]
|
||||
"src/lerobot/policies/vqbet/vqbet_utils.py" = ["D"]
|
||||
"src/lerobot/policies/wall_x/constant.py" = ["D"]
|
||||
"src/lerobot/policies/wall_x/qwen_model/**" = ["D"]
|
||||
"src/lerobot/policies/wall_x/utils.py" = ["D"]
|
||||
"src/lerobot/policies/xvla/action_hub.py" = ["D"]
|
||||
"src/lerobot/policies/xvla/soft_transformer.py" = ["D"]
|
||||
"src/lerobot/policies/xvla/utils.py" = ["D"]
|
||||
"src/lerobot/policies/**" = ["D"]
|
||||
"src/lerobot/processor/**" = ["D"]
|
||||
"src/lerobot/rewards/**" = ["D"]
|
||||
"src/lerobot/rl/**" = ["D"]
|
||||
@@ -552,7 +520,7 @@ ignore-private = false
|
||||
ignore-property-decorators = false
|
||||
ignore-module = false
|
||||
ignore-setters = false
|
||||
fail-under = 58
|
||||
fail-under = 55
|
||||
output-format = "term-missing"
|
||||
color = true
|
||||
paths = ["src/lerobot"]
|
||||
|
||||
@@ -22,7 +22,7 @@ Import them directly: ``from lerobot.configs.train import TrainPipelineConfig``
|
||||
"""
|
||||
|
||||
from .dataset import DatasetRecordConfig
|
||||
from .default import DatasetConfig, EvalConfig, JobConfig, PeftConfig, WandBConfig
|
||||
from .default import DatasetConfig, EMAConfig, EvalConfig, JobConfig, PeftConfig, WandBConfig
|
||||
from .policies import PreTrainedConfig
|
||||
from .recipe import MessageTurn, TrainingRecipe, load_recipe
|
||||
from .types import (
|
||||
@@ -57,6 +57,7 @@ __all__ = [
|
||||
# Config classes
|
||||
"DatasetRecordConfig",
|
||||
"DatasetConfig",
|
||||
"EMAConfig",
|
||||
"EvalConfig",
|
||||
"JobConfig",
|
||||
"MessageTurn",
|
||||
|
||||
@@ -139,6 +139,59 @@ class EvalConfig:
|
||||
return min(by_cpu, self.n_episodes, 64)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EMAConfig:
|
||||
"""Exponential moving average (EMA) of the policy weights.
|
||||
|
||||
Standard practice for diffusion-style policies (Chi et al. 2023, "Diffusion Policy", section V.D):
|
||||
the reference implementation enables it in every config and evaluates the EMA weights. Off by
|
||||
default here because it keeps a second full copy of the parameters in memory.
|
||||
|
||||
The decay follows the warmup schedule from diffusers' `EMAModel`:
|
||||
`decay_t = 1 - (1 + t / inv_gamma) ** -power`, clamped to `[min_decay, max_decay]`.
|
||||
The defaults mirror the reference implementation. Alternatively, set `decay` for a constant
|
||||
decay at every step, as used by openpi for pi0/pi05 (`ema_decay=0.99`).
|
||||
"""
|
||||
|
||||
enable: bool = False
|
||||
# Constant decay coefficient (openpi-style, e.g. 0.99 for pi0/pi05). When set, the warmup
|
||||
# schedule below is bypassed and the shadow uses this decay at every step.
|
||||
decay: float | None = None
|
||||
# Number of optimizer steps during which the shadow stays a hard copy of the live weights.
|
||||
update_after_step: int = 0
|
||||
# Warmup schedule parameters (see class docstring).
|
||||
inv_gamma: float = 1.0
|
||||
power: float = 0.75
|
||||
min_decay: float = 0.0
|
||||
max_decay: float = 0.9999
|
||||
# Evaluate the EMA weights (instead of the live ones) during periodic env eval.
|
||||
# Offline eval-loss (--eval_steps) always uses the live weights: it runs on every rank
|
||||
# while the EMA shadow only lives on the main process.
|
||||
use_for_eval: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (0.0 <= self.min_decay <= self.max_decay <= 1.0):
|
||||
raise ValueError(
|
||||
"Expected 0 <= ema.min_decay <= ema.max_decay <= 1, got "
|
||||
f"min_decay={self.min_decay} and max_decay={self.max_decay}."
|
||||
)
|
||||
if self.inv_gamma <= 0:
|
||||
raise ValueError(f"ema.inv_gamma must be positive, got {self.inv_gamma}.")
|
||||
if self.power <= 0:
|
||||
raise ValueError(f"ema.power must be positive, got {self.power}.")
|
||||
if self.update_after_step < 0:
|
||||
raise ValueError(f"ema.update_after_step must be >= 0, got {self.update_after_step}.")
|
||||
if self.decay is not None:
|
||||
if not 0.0 <= self.decay <= 1.0:
|
||||
raise ValueError(f"ema.decay must be in [0, 1], got {self.decay}.")
|
||||
# Keep the literals in sync with the field defaults above.
|
||||
if self.min_decay != 0.0 or self.max_decay != 0.9999:
|
||||
raise ValueError(
|
||||
"ema.decay (constant decay) and ema.min_decay/ema.max_decay (schedule clamp) are "
|
||||
"mutually exclusive: set one or the other."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PeftConfig:
|
||||
# PEFT offers many fine-tuning methods, layer adapters being the most common and currently also the most
|
||||
|
||||
@@ -35,7 +35,7 @@ from lerobot.utils.hub import HubMixin, find_latest_hub_checkpoint
|
||||
from lerobot.utils.sample_weighting import SampleWeightingConfig
|
||||
|
||||
from . import parser
|
||||
from .default import DatasetConfig, EvalConfig, JobConfig, PeftConfig, WandBConfig
|
||||
from .default import DatasetConfig, EMAConfig, EvalConfig, JobConfig, PeftConfig, WandBConfig
|
||||
from .policies import PreTrainedConfig
|
||||
from .rewards import RewardModelConfig
|
||||
|
||||
@@ -163,6 +163,8 @@ class TrainPipelineConfig(HubMixin):
|
||||
# FSDP/DDP tuning knobs, compile & activation-checkpointing placeholders.
|
||||
accelerator: AcceleratorConfig = field(default_factory=AcceleratorConfig)
|
||||
eval: EvalConfig = field(default_factory=EvalConfig)
|
||||
# Maintain an EMA shadow of the policy weights during training (see EMAConfig).
|
||||
ema: EMAConfig = field(default_factory=EMAConfig)
|
||||
wandb: WandBConfig = field(default_factory=WandBConfig)
|
||||
peft: PeftConfig | None = None
|
||||
|
||||
|
||||
@@ -613,8 +613,15 @@ def aggregate_feature_stats(stats_ft_list: list[dict[str, dict]]) -> dict[str, d
|
||||
for q_key in quantile_keys:
|
||||
if all(q_key in s for s in stats_ft_list):
|
||||
quantile_values = np.stack([s[q_key] for s in stats_ft_list])
|
||||
weighted_quantiles = quantile_values * counts
|
||||
aggregated[q_key] = weighted_quantiles.sum(axis=0) / total_count
|
||||
# Exact global quantiles cannot be recovered from quantile summaries.
|
||||
# Keep a conservative envelope of the available estimates: min
|
||||
# for lower quantiles and max for upper quantiles. The resulting
|
||||
# values are bounds across the inputs, not global quantile estimates.
|
||||
q_percent = int(q_key[1:])
|
||||
if q_percent <= 50:
|
||||
aggregated[q_key] = np.min(quantile_values, axis=0)
|
||||
else:
|
||||
aggregated[q_key] = np.max(quantile_values, axis=0)
|
||||
|
||||
return aggregated
|
||||
|
||||
|
||||
@@ -33,11 +33,7 @@ if TYPE_CHECKING:
|
||||
from lerobot.policies.pretrained import PreTrainedPolicy
|
||||
|
||||
# Env vars through which `accelerate launch --config_file` (or a stray shell) would configure
|
||||
# accelerate behind the config system's back. Plugin `__post_init__`s read these silently as
|
||||
# field fallbacks (ACCELERATE_DYNAMO_* enables torch.compile through the default
|
||||
# TorchDynamoPlugin; ACCELERATE_GRADIENT_ACCUMULATION_STEPS overrides the explicitly passed
|
||||
# value inside Accelerator.__init__), which would make train_config.json lie about what ran.
|
||||
_ACCELERATE_ENV_PREFIXES = ("FSDP_", "PARALLELISM_CONFIG_", "ACCELERATE_DYNAMO_")
|
||||
# accelerate behind the config system's back, making train_config.json lie about what ran.
|
||||
_ACCELERATE_ENV_VARS = (
|
||||
"ACCELERATE_USE_FSDP",
|
||||
"ACCELERATE_USE_PARALLELISM_CONFIG",
|
||||
@@ -59,11 +55,7 @@ def guard_against_env_interference() -> None:
|
||||
"""
|
||||
if os.environ.get(_ENV_OVERRIDE):
|
||||
return
|
||||
offending = sorted(
|
||||
name
|
||||
for name in os.environ
|
||||
if name in _ACCELERATE_ENV_VARS or name.startswith(_ACCELERATE_ENV_PREFIXES)
|
||||
)
|
||||
offending = sorted(name for name in _ACCELERATE_ENV_VARS if name in os.environ)
|
||||
if offending:
|
||||
raise RuntimeError(
|
||||
f"Accelerate-configuring environment variables are set: {', '.join(offending)}. "
|
||||
|
||||
@@ -40,93 +40,44 @@ class ACTConfig(PreTrainedConfig):
|
||||
- "action" is required as an output key.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back). ACT only supports a value of 1; anything else raises in
|
||||
`__post_init__`.
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy
|
||||
is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 100):
|
||||
The size of the action prediction "chunks" in units of environment steps.
|
||||
n_action_steps (`int`, *optional*, defaults to 100):
|
||||
The number of action steps to run in the environment for one invocation of the policy. This
|
||||
should be no greater than `chunk_size`. For example, if the chunk size is 100, you may set this
|
||||
to 50: the model predicts 100 steps worth of actions, runs 50 in the environment, and throws
|
||||
the other 50 out.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to mean/std normalization for visual, state, and action features.
|
||||
vision_backbone (`str`, *optional*, defaults to `"resnet18"`):
|
||||
Name of the torchvision resnet backbone to use for encoding images.
|
||||
pretrained_backbone_weights (`str | None`, *optional*, defaults to `"ResNet18_Weights.IMAGENET1K_V1"`):
|
||||
Pretrained weights from torchvision to initialize the backbone. `None` means no pretrained
|
||||
weights.
|
||||
replace_final_stride_with_dilation (`int`, *optional*, defaults to `False`):
|
||||
Whether to replace the ResNet's final 2x2 stride with a dilated convolution.
|
||||
pre_norm (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use "pre-norm" in the transformer blocks.
|
||||
dim_model (`int`, *optional*, defaults to 512):
|
||||
The transformer blocks' main hidden dimension.
|
||||
n_heads (`int`, *optional*, defaults to 8):
|
||||
The number of heads to use in the transformer blocks' multi-head attention.
|
||||
dim_feedforward (`int`, *optional*, defaults to 3200):
|
||||
The dimension to expand the transformer's hidden dimension to in the feed-forward layers.
|
||||
feedforward_activation (`str`, *optional*, defaults to `"relu"`):
|
||||
The activation to use in the transformer block's feed-forward layers.
|
||||
n_encoder_layers (`int`, *optional*, defaults to 4):
|
||||
The number of transformer layers to use for the transformer encoder.
|
||||
n_decoder_layers (`int`, *optional*, defaults to 1):
|
||||
The number of transformer layers to use for the transformer decoder.
|
||||
use_vae (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use a variational objective during training. This introduces another transformer
|
||||
n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the
|
||||
current step and additional steps going back).
|
||||
chunk_size: The size of the action prediction "chunks" in units of environment steps.
|
||||
n_action_steps: The number of action steps to run in the environment for one invocation of the policy.
|
||||
This should be no greater than the chunk size. For example, if the chunk size size 100, you may
|
||||
set this to 50. This would mean that the model predicts 100 steps worth of actions, runs 50 in the
|
||||
environment, and throws the other 50 out.
|
||||
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents
|
||||
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents
|
||||
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
||||
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
||||
vision_backbone: Name of the torchvision resnet backbone to use for encoding images.
|
||||
pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone.
|
||||
`None` means no pretrained weights.
|
||||
replace_final_stride_with_dilation: Whether to replace the ResNet's final 2x2 stride with a dilated
|
||||
convolution.
|
||||
pre_norm: Whether to use "pre-norm" in the transformer blocks.
|
||||
dim_model: The transformer blocks' main hidden dimension.
|
||||
n_heads: The number of heads to use in the transformer blocks' multi-head attention.
|
||||
dim_feedforward: The dimension to expand the transformer's hidden dimension to in the feed-forward
|
||||
layers.
|
||||
feedforward_activation: The activation to use in the transformer block's feed-forward layers.
|
||||
n_encoder_layers: The number of transformer layers to use for the transformer encoder.
|
||||
n_decoder_layers: The number of transformer layers to use for the transformer decoder.
|
||||
use_vae: Whether to use a variational objective during training. This introduces another transformer
|
||||
which is used as the VAE's encoder (not to be confused with the transformer encoder - see
|
||||
documentation in the policy class).
|
||||
latent_dim (`int`, *optional*, defaults to 32):
|
||||
The VAE's latent dimension.
|
||||
n_vae_encoder_layers (`int`, *optional*, defaults to 4):
|
||||
The number of transformer layers to use for the VAE's encoder.
|
||||
temporal_ensemble_coeff (`float | None`, *optional*):
|
||||
Coefficient for the exponential weighting scheme to apply for temporal ensembling. `None` (the
|
||||
default) means temporal ensembling is not used. `n_action_steps` must be 1 when using this
|
||||
feature, as inference needs to happen at every step to form an ensemble. For more information
|
||||
on how ensembling works, see `ACTTemporalEnsembler`.
|
||||
dropout (`float`, *optional*, defaults to 0.1):
|
||||
Dropout to use in the transformer layers (see code for details).
|
||||
kl_weight (`float`, *optional*, defaults to 10.0):
|
||||
The weight to use for the KL-divergence component of the loss if the variational objective is
|
||||
enabled. Loss is then calculated as: `reconstruction_loss + kl_weight * kld_loss`.
|
||||
optimizer_lr (`float`, *optional*, defaults to 1e-05):
|
||||
Learning rate for the AdamW optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.0001):
|
||||
Weight decay for the AdamW optimizer preset.
|
||||
optimizer_lr_backbone (`float`, *optional*, defaults to 1e-05):
|
||||
Learning rate for the vision backbone's parameters in the AdamW optimizer preset.
|
||||
latent_dim: The VAE's latent dimension.
|
||||
n_vae_encoder_layers: The number of transformer layers to use for the VAE's encoder.
|
||||
temporal_ensemble_coeff: Coefficient for the exponential weighting scheme to apply for temporal
|
||||
ensembling. Defaults to None which means temporal ensembling is not used. `n_action_steps` must be
|
||||
1 when using this feature, as inference needs to happen at every step to form an ensemble. For
|
||||
more information on how ensembling works, please see `ACTTemporalEnsembler`.
|
||||
dropout: Dropout to use in the transformer layers (see code for details).
|
||||
kl_weight: The weight to use for the KL-divergence component of the loss if the variational objective
|
||||
is enabled. Loss is then calculated as: `reconstruction_loss + kl_weight * kld_loss`.
|
||||
"""
|
||||
|
||||
# Input / output structure.
|
||||
@@ -177,9 +128,9 @@ class ACTConfig(PreTrainedConfig):
|
||||
optimizer_lr_backbone: float = 1e-5
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates `vision_backbone`, `temporal_ensemble_coeff`/`n_action_steps`, `n_action_steps`/`chunk_size`, and `n_obs_steps`."""
|
||||
super().__post_init__()
|
||||
|
||||
"""Input validation (not exhaustive)."""
|
||||
if not self.vision_backbone.startswith("resnet"):
|
||||
raise ValueError(
|
||||
f"`vision_backbone` must be one of the ResNet variants. Got {self.vision_backbone}."
|
||||
@@ -200,32 +151,26 @@ class ACTConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
weight_decay=self.optimizer_weight_decay,
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return None
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if not self.image_features and not self.env_state_feature:
|
||||
raise ValueError("You must provide at least one image or the environment state among the inputs.")
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -40,7 +40,8 @@ from .configuration_act import ACTConfig
|
||||
|
||||
|
||||
class ACTPolicy(PreTrainedPolicy):
|
||||
"""Action Chunking Transformer Policy as per Learning Fine-Grained Bimanual Manipulation with Low-Cost
|
||||
"""
|
||||
Action Chunking Transformer Policy as per Learning Fine-Grained Bimanual Manipulation with Low-Cost
|
||||
Hardware (paper: https://huggingface.co/papers/2304.13705, code: https://github.com/tonyzhaozh/act)
|
||||
"""
|
||||
|
||||
@@ -54,11 +55,10 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
config: ACTConfig,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the ACT model (and, if enabled, the temporal ensembler) from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`ACTConfig`):
|
||||
Policy configuration.
|
||||
config: Policy configuration class instance or None, in which case the default instantiation of
|
||||
the configuration class is used.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -72,11 +72,6 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Splits parameters into two groups: the vision backbone, trained at `optimizer_lr_backbone`, and
|
||||
everything else, trained at the base `optimizer_lr`.
|
||||
"""
|
||||
# TODO(aliberts, rcadene): As of now, lr_backbone == lr
|
||||
# Should we remove this and just `return self.parameters()`?
|
||||
return [
|
||||
@@ -98,11 +93,7 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
]
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Resets the `ACTTemporalEnsembler` when temporal ensembling is enabled, otherwise clears the action
|
||||
queue consumed by `select_action`.
|
||||
"""
|
||||
"""This should be called whenever the environment is reset."""
|
||||
if self.config.temporal_ensemble_coeff is not None:
|
||||
self.temporal_ensembler.reset()
|
||||
else:
|
||||
@@ -110,11 +101,11 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
"""Select a single action given environment observations.
|
||||
|
||||
Returns one action at a time from a queue populated by `predict_action_chunk`, refilling it once
|
||||
it runs dry. When temporal ensembling is enabled, the queue is bypassed and the action is instead
|
||||
produced by combining chunks via `ACTTemporalEnsembler`.
|
||||
This method wraps `select_actions` in order to return one action at a time for execution in the
|
||||
environment. It works by managing the actions in a queue and only calling `select_actions` when the
|
||||
queue is empty.
|
||||
"""
|
||||
self.eval() # keeping the policy in eval mode as it could be set to train mode while queue is consumed
|
||||
|
||||
@@ -135,7 +126,7 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]."""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
self.eval()
|
||||
|
||||
if self.config.image_features:
|
||||
@@ -146,11 +137,7 @@ class ACTPolicy(PreTrainedPolicy):
|
||||
return actions
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
The loss is an L1 reconstruction loss between the predicted and target actions, plus (when
|
||||
`use_vae` is enabled) a KL-divergence term weighted by `kl_weight`.
|
||||
"""
|
||||
"""Run the batch through the model and compute the loss for training or validation."""
|
||||
if self.config.image_features:
|
||||
batch = dict(batch) # shallow copy so that adding a key doesn't modify the original
|
||||
batch[OBS_IMAGES] = [batch[key] for key in self.config.image_features]
|
||||
@@ -234,7 +221,8 @@ class ACTTemporalEnsembler:
|
||||
self.ensembled_actions_count = None
|
||||
|
||||
def update(self, actions: Tensor) -> Tensor:
|
||||
"""Takes a (batch, chunk_size, action_dim) sequence of actions, update the temporal ensemble for all
|
||||
"""
|
||||
Takes a (batch, chunk_size, action_dim) sequence of actions, update the temporal ensemble for all
|
||||
time steps, and pop/return the next batch of actions in the sequence.
|
||||
"""
|
||||
self.ensemble_weights = self.ensemble_weights.to(device=actions.device)
|
||||
@@ -638,13 +626,13 @@ class ACTDecoderLayer(nn.Module):
|
||||
decoder_pos_embed: Tensor | None = None,
|
||||
encoder_pos_embed: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: (Decoder Sequence, Batch, Channel) tensor of input tokens.
|
||||
encoder_out: (Encoder Sequence, B, C) output features from the last layer of the encoder we are
|
||||
cross-attending with.
|
||||
encoder_pos_embed: (ES, 1, C) positional embedding for keys (from the encoder).
|
||||
decoder_pos_embed: (DS, 1, C) positional embedding for the queries (from the decoder).
|
||||
|
||||
Returns:
|
||||
(DS, B, C) tensor of decoder output features.
|
||||
"""
|
||||
@@ -683,11 +671,9 @@ def create_sinusoidal_pos_embedding(num_positions: int, dimension: int) -> Tenso
|
||||
"""1D sinusoidal positional embeddings as in Attention is All You Need.
|
||||
|
||||
Args:
|
||||
num_positions (`int`): Number of positions to embed (the sequence length).
|
||||
dimension (`int`): The embedding dimension.
|
||||
num_positions: Number of token positions required.
|
||||
Returns: (num_positions, dimension) position embeddings (the first dimension is the batch dimension).
|
||||
|
||||
Returns:
|
||||
`(num_positions, dimension)` position embeddings (the first dimension is the batch dimension).
|
||||
"""
|
||||
|
||||
def get_position_angle_vec(position):
|
||||
@@ -707,8 +693,9 @@ class ACTSinusoidalPositionEmbedding2d(nn.Module):
|
||||
"""
|
||||
|
||||
def __init__(self, dimension: int):
|
||||
"""Args:
|
||||
dimension: The desired dimension of the embeddings.
|
||||
"""
|
||||
Args:
|
||||
dimension: The desired dimension of the embeddings.
|
||||
"""
|
||||
super().__init__()
|
||||
self.dimension = dimension
|
||||
@@ -718,9 +705,9 @@ class ACTSinusoidalPositionEmbedding2d(nn.Module):
|
||||
self._temperature = 10000
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: A (B, C, H, W) batch of 2D feature map to generate the embeddings for.
|
||||
|
||||
Returns:
|
||||
A (1, C, H, W) batch of corresponding sinusoidal positional embeddings.
|
||||
"""
|
||||
|
||||
@@ -40,7 +40,7 @@ def make_act_pre_post_processors(
|
||||
|
||||
Args:
|
||||
config (ACTConfig): The ACT policy configuration object.
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): A dictionary containing dataset
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None): A dictionary containing dataset
|
||||
statistics (e.g., mean and std) used for normalization. Defaults to None.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -41,135 +41,63 @@ class DiffusionConfig(PreTrainedConfig):
|
||||
- "action" is required as an output key.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 2):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy
|
||||
is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
horizon (`int`, *optional*, defaults to 64):
|
||||
Diffusion model action prediction size as detailed in `DiffusionPolicy.select_action`.
|
||||
n_action_steps (`int`, *optional*, defaults to 32):
|
||||
The number of action steps to run in the environment for one invocation of the policy. See
|
||||
`DiffusionPolicy.select_action` for more details.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to mean/std normalization for visual features and min/max normalization for
|
||||
state and action features.
|
||||
drop_n_last_frames (`int`, *optional*, defaults to 7):
|
||||
Number of frames dropped from the end of each episode when sampling training windows, which
|
||||
avoids excessive padding. Should track `horizon - n_action_steps - n_obs_steps + 1`.
|
||||
vision_backbone (`str`, *optional*, defaults to `"resnet18"`):
|
||||
Name of the torchvision resnet backbone to use for encoding images.
|
||||
resize_shape (`tuple[int, int] | None`, *optional*):
|
||||
(H, W) shape to resize images to as a preprocessing step for the vision backbone. `None`
|
||||
disables resizing, so the original image resolution is used.
|
||||
crop_ratio (`float`, *optional*, defaults to 1.0):
|
||||
Ratio in (0, 1] used to derive the crop size from `resize_shape` (`crop_h =
|
||||
int(resize_shape[0] * crop_ratio)`, likewise for width). Set to 1.0 to disable cropping. Only
|
||||
takes effect when `resize_shape` is not `None`.
|
||||
crop_shape (`tuple[int, int] | None`, *optional*):
|
||||
(H, W) shape to crop images to. Computed automatically when `resize_shape` is set and
|
||||
`crop_ratio` < 1.0. Can also be set directly for legacy configs that use crop-only (without
|
||||
resize). `None`, with no derivation applying, means no cropping.
|
||||
crop_is_random (`bool`, *optional*, defaults to `True`):
|
||||
Whether the crop should be random at training time (it's always a center crop in eval mode).
|
||||
pretrained_backbone_weights (`str | None`, *optional*, defaults to `"ResNet18_Weights.IMAGENET1K_V1"`):
|
||||
Pretrained weights from torchvision to initialize the backbone. `None` means no pretrained
|
||||
weights.
|
||||
use_group_norm (`bool`, *optional*, defaults to `False`):
|
||||
Whether to replace batch normalization with group normalization in the backbone. The group
|
||||
sizes are set to be about 16 (`feature_dim // 16`).
|
||||
spatial_softmax_num_keypoints (`int`, *optional*, defaults to 32):
|
||||
Number of keypoints for SpatialSoftmax.
|
||||
use_separate_rgb_encoder_per_camera (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use a separate RGB encoder for each camera view.
|
||||
down_dims (`tuple[int, ...]`, *optional*, defaults to `(512, 1024, 2048)`):
|
||||
Feature dimension for each stage of temporal downsampling in the diffusion modeling Unet. You
|
||||
may provide a variable number of dimensions, therefore also controlling the degree of
|
||||
n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the
|
||||
current step and additional steps going back).
|
||||
horizon: Diffusion model action prediction size as detailed in `DiffusionPolicy.select_action`.
|
||||
n_action_steps: The number of action steps to run in the environment for one invocation of the policy.
|
||||
See `DiffusionPolicy.select_action` for more details.
|
||||
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents
|
||||
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents
|
||||
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
||||
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
||||
vision_backbone: Name of the torchvision resnet backbone to use for encoding images.
|
||||
resize_shape: (H, W) shape to resize images to as a preprocessing step for the vision
|
||||
backbone. If None, no resizing is done and the original image resolution is used.
|
||||
crop_ratio: Ratio in (0, 1] used to derive the crop size from resize_shape
|
||||
(crop_h = int(resize_shape[0] * crop_ratio), likewise for width).
|
||||
Set to 1.0 to disable cropping. Only takes effect when resize_shape is not None.
|
||||
crop_shape: (H, W) shape to crop images to. When resize_shape is set and crop_ratio < 1.0,
|
||||
this is computed automatically. Can also be set directly for legacy configs that use
|
||||
crop-only (without resize). If None and no derivation applies, no cropping is done.
|
||||
crop_is_random: Whether the crop should be random at training time (it's always a center
|
||||
crop in eval mode).
|
||||
pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone.
|
||||
`None` means no pretrained weights.
|
||||
use_group_norm: Whether to replace batch normalization with group normalization in the backbone.
|
||||
The group sizes are set to be about 16 (to be precise, feature_dim // 16).
|
||||
spatial_softmax_num_keypoints: Number of keypoints for SpatialSoftmax.
|
||||
use_separate_rgb_encoder_per_camera: Whether to use a separate RGB encoder for each camera view.
|
||||
down_dims: Feature dimension for each stage of temporal downsampling in the diffusion modeling Unet.
|
||||
You may provide a variable number of dimensions, therefore also controlling the degree of
|
||||
downsampling.
|
||||
kernel_size (`int`, *optional*, defaults to 5):
|
||||
The convolutional kernel size of the diffusion modeling Unet.
|
||||
n_groups (`int`, *optional*, defaults to 8):
|
||||
Number of groups used in the group norm of the Unet's convolutional blocks.
|
||||
diffusion_step_embed_dim (`int`, *optional*, defaults to 128):
|
||||
The Unet is conditioned on the diffusion timestep via a small non-linear network. This is the
|
||||
output dimension of that network, i.e. the embedding dimension.
|
||||
use_film_scale_modulation (`bool`, *optional*, defaults to `True`):
|
||||
FiLM (https://huggingface.co/papers/1709.07871) is used for the Unet conditioning. Bias
|
||||
modulation is used by default, while this parameter indicates whether to also use scale
|
||||
kernel_size: The convolutional kernel size of the diffusion modeling Unet.
|
||||
n_groups: Number of groups used in the group norm of the Unet's convolutional blocks.
|
||||
diffusion_step_embed_dim: The Unet is conditioned on the diffusion timestep via a small non-linear
|
||||
network. This is the output dimension of that network, i.e., the embedding dimension.
|
||||
use_film_scale_modulation: FiLM (https://huggingface.co/papers/1709.07871) is used for the Unet conditioning.
|
||||
Bias modulation is used be default, while this parameter indicates whether to also use scale
|
||||
modulation.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to checkpoint the Unet residual blocks during training. This reduces activation memory
|
||||
at the cost of recomputing those blocks during the backward pass.
|
||||
noise_scheduler_type (`str`, *optional*, defaults to `"DDPM"`):
|
||||
Name of the noise scheduler to use. Supported options: `"DDPM"`, `"DDIM"`.
|
||||
num_train_timesteps (`int`, *optional*, defaults to 100):
|
||||
Number of diffusion steps for the forward diffusion schedule.
|
||||
beta_schedule (`str`, *optional*, defaults to `"squaredcos_cap_v2"`):
|
||||
Name of the diffusion beta schedule as per `DDPMScheduler` from Hugging Face diffusers.
|
||||
beta_start (`float`, *optional*, defaults to 0.0001):
|
||||
Beta value for the first forward-diffusion step.
|
||||
beta_end (`float`, *optional*, defaults to 0.02):
|
||||
Beta value for the last forward-diffusion step.
|
||||
prediction_type (`str`, *optional*, defaults to `"epsilon"`):
|
||||
The type of prediction that the diffusion modeling Unet makes. Choose from `"epsilon"` or
|
||||
`"sample"`. These have equivalent outcomes from a latent variable modeling perspective, but
|
||||
`"epsilon"` has been shown to work better in many deep neural network settings.
|
||||
clip_sample (`bool`, *optional*, defaults to `True`):
|
||||
Whether to clip the sample to `[-clip_sample_range, +clip_sample_range]` for each denoising
|
||||
step at inference time. This requires the action space to be normalized to fit within that
|
||||
range.
|
||||
clip_sample_range (`float`, *optional*, defaults to 1.0):
|
||||
The magnitude of the clipping range described above.
|
||||
num_inference_steps (`int | None`, *optional*):
|
||||
Number of reverse diffusion steps to use at inference time (steps are evenly spaced). If not
|
||||
provided, defaults to the same value as `num_train_timesteps`.
|
||||
compile_model (`bool`, *optional*, defaults to `False`):
|
||||
Whether to compile the Unet with `torch.compile`.
|
||||
compile_mode (`str`, *optional*, defaults to `"reduce-overhead"`):
|
||||
`torch.compile` mode to use when `compile_model` is enabled.
|
||||
do_mask_loss_for_padding (`bool`, *optional*, defaults to `False`):
|
||||
Whether to mask the loss when there are copy-padded actions. See `LeRobotDataset` and
|
||||
`load_previous_and_future_frames` for more information. This defaults to `False` as the
|
||||
original Diffusion Policy implementation does the same.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Learning rate for the Adam optimizer preset.
|
||||
optimizer_betas (`tuple`, *optional*, defaults to `(0.95, 0.999)`):
|
||||
Adam optimizer's beta coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Adam optimizer's epsilon for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-06):
|
||||
Weight decay for the Adam optimizer preset.
|
||||
scheduler_name (`str`, *optional*, defaults to `"cosine"`):
|
||||
Name of the LR scheduler preset to use.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 500):
|
||||
Number of warmup steps for the LR scheduler preset.
|
||||
gradient_checkpointing: Whether to checkpoint the Unet residual blocks during training. This reduces
|
||||
activation memory at the cost of recomputing those blocks during the backward pass.
|
||||
noise_scheduler_type: Name of the noise scheduler to use. Supported options: ["DDPM", "DDIM"].
|
||||
num_train_timesteps: Number of diffusion steps for the forward diffusion schedule.
|
||||
beta_schedule: Name of the diffusion beta schedule as per DDPMScheduler from Hugging Face diffusers.
|
||||
beta_start: Beta value for the first forward-diffusion step.
|
||||
beta_end: Beta value for the last forward-diffusion step.
|
||||
prediction_type: The type of prediction that the diffusion modeling Unet makes. Choose from "epsilon"
|
||||
or "sample". These have equivalent outcomes from a latent variable modeling perspective, but
|
||||
"epsilon" has been shown to work better in many deep neural network settings.
|
||||
clip_sample: Whether to clip the sample to [-`clip_sample_range`, +`clip_sample_range`] for each
|
||||
denoising step at inference time. WARNING: you will need to make sure your action-space is
|
||||
normalized to fit within this range.
|
||||
clip_sample_range: The magnitude of the clipping range as described above.
|
||||
num_inference_steps: Number of reverse diffusion steps to use at inference time (steps are evenly
|
||||
spaced). If not provided, this defaults to be the same as `num_train_timesteps`.
|
||||
do_mask_loss_for_padding: Whether to mask the loss when there are copy-padded actions. See
|
||||
`LeRobotDataset` and `load_previous_and_future_frames` for more information. Note, this defaults
|
||||
to False as the original Diffusion Policy implementation does the same.
|
||||
"""
|
||||
|
||||
# Inputs / output structure.
|
||||
@@ -236,9 +164,9 @@ class DiffusionConfig(PreTrainedConfig):
|
||||
scheduler_warmup_steps: int = 500
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates image/state feature presence and normalization-mode compatibility with the configured vision backbone."""
|
||||
super().__post_init__()
|
||||
|
||||
"""Input validation (not exhaustive)."""
|
||||
if not self.vision_backbone.startswith("resnet"):
|
||||
raise ValueError(
|
||||
f"`vision_backbone` must be one of the ResNet variants. Got {self.vision_backbone}."
|
||||
@@ -285,7 +213,6 @@ class DiffusionConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -294,14 +221,12 @@ class DiffusionConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> DiffuserSchedulerConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return DiffuserSchedulerConfig(
|
||||
name=self.scheduler_name,
|
||||
num_warmup_steps=self.scheduler_warmup_steps,
|
||||
)
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if len(self.image_features) == 0 and self.env_state_feature is None:
|
||||
raise ValueError("You must provide at least one image or the environment state among the inputs.")
|
||||
|
||||
@@ -324,15 +249,12 @@ class DiffusionConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, 1))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, 1 - self.n_obs_steps + self.horizon))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -54,7 +54,8 @@ from .configuration_diffusion import DiffusionConfig
|
||||
|
||||
|
||||
class DiffusionPolicy(PreTrainedPolicy):
|
||||
"""Diffusion Policy as per "Diffusion Policy: Visuomotor Policy Learning via Action Diffusion"
|
||||
"""
|
||||
Diffusion Policy as per "Diffusion Policy: Visuomotor Policy Learning via Action Diffusion"
|
||||
(paper: https://huggingface.co/papers/2303.04137, code: https://github.com/real-stanford/diffusion_policy).
|
||||
"""
|
||||
|
||||
@@ -66,11 +67,12 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
config: DiffusionConfig,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the diffusion model from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`DiffusionConfig`):
|
||||
Policy configuration.
|
||||
config: Policy configuration class instance or None, in which case the default instantiation of
|
||||
the configuration class is used.
|
||||
dataset_stats: Dataset statistics to be used for normalization. If not passed here, it is expected
|
||||
that they will be passed with a call to `load_state_dict` before the policy is used.
|
||||
"""
|
||||
require_package("diffusers", extra="diffusion")
|
||||
super().__init__(config)
|
||||
@@ -85,14 +87,10 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.diffusion.parameters()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Clears the observation and action queues consumed by `select_action`.
|
||||
"""
|
||||
"""Clear observation and action queues. Should be called on `env.reset()`"""
|
||||
self._queues = {
|
||||
OBS_STATE: deque(maxlen=self.config.n_obs_steps),
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
@@ -104,7 +102,7 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
"""Predict a chunk of actions given environment observations.
|
||||
|
||||
Supports two modes:
|
||||
- Online (queues populated via select_action): stacks observations from internal queues.
|
||||
@@ -125,7 +123,7 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
"""Select a single action given environment observations.
|
||||
|
||||
This method handles caching a history of observations and an action trajectory generated by the
|
||||
underlying diffusion model. Here's how it works:
|
||||
@@ -163,7 +161,7 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
return action
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, None]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]."""
|
||||
"""Run the batch through the model and compute the loss for training or validation."""
|
||||
if self.config.image_features:
|
||||
batch = dict(batch) # shallow copy so that adding a key doesn't modify the original
|
||||
for key in self.config.image_features:
|
||||
@@ -176,7 +174,8 @@ class DiffusionPolicy(PreTrainedPolicy):
|
||||
|
||||
|
||||
def _make_noise_scheduler(name: str, **kwargs: dict):
|
||||
"""Factory for noise scheduler instances of the requested type. All kwargs are passed
|
||||
"""
|
||||
Factory for noise scheduler instances of the requested type. All kwargs are passed
|
||||
to the scheduler.
|
||||
"""
|
||||
require_package("diffusers", extra="diffusion")
|
||||
@@ -307,7 +306,8 @@ class DiffusionModel(nn.Module):
|
||||
return torch.cat(global_cond_feats, dim=-1).flatten(start_dim=1)
|
||||
|
||||
def generate_actions(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||
"""This function expects `batch` to have:
|
||||
"""
|
||||
This function expects `batch` to have:
|
||||
{
|
||||
"observation.state": (B, n_obs_steps, state_dim)
|
||||
|
||||
@@ -333,7 +333,8 @@ class DiffusionModel(nn.Module):
|
||||
return actions
|
||||
|
||||
def compute_loss(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""This function expects `batch` to have (at least):
|
||||
"""
|
||||
This function expects `batch` to have (at least):
|
||||
{
|
||||
"observation.state": (B, n_obs_steps, state_dim)
|
||||
|
||||
@@ -400,7 +401,8 @@ class DiffusionModel(nn.Module):
|
||||
|
||||
|
||||
class SpatialSoftmax(nn.Module):
|
||||
"""Spatial Soft Argmax operation described in "Deep Spatial Autoencoders for Visuomotor Learning" by Finn et al.
|
||||
"""
|
||||
Spatial Soft Argmax operation described in "Deep Spatial Autoencoders for Visuomotor Learning" by Finn et al.
|
||||
(https://huggingface.co/papers/1509.06113). A minimal port of the robomimic implementation.
|
||||
|
||||
At a high level, this takes 2D feature maps (from a convnet/ViT) and returns the "center of mass"
|
||||
@@ -422,9 +424,10 @@ class SpatialSoftmax(nn.Module):
|
||||
"""
|
||||
|
||||
def __init__(self, input_shape, num_kp=None):
|
||||
"""Args:
|
||||
input_shape (list): (C, H, W) input feature map shape.
|
||||
num_kp (int): number of keypoints in output. If None, output will have the same number of channels as input.
|
||||
"""
|
||||
Args:
|
||||
input_shape (list): (C, H, W) input feature map shape.
|
||||
num_kp (int): number of keypoints in output. If None, output will have the same number of channels as input.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
@@ -447,9 +450,9 @@ class SpatialSoftmax(nn.Module):
|
||||
self.register_buffer("pos_grid", torch.cat([pos_x, pos_y], dim=1))
|
||||
|
||||
def forward(self, features: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
features: (B, C, H, W) input feature maps.
|
||||
|
||||
Returns:
|
||||
(B, K, 2) image-space coordinates of keypoints.
|
||||
"""
|
||||
@@ -533,9 +536,9 @@ class DiffusionRgbEncoder(nn.Module):
|
||||
self.relu = nn.ReLU()
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: (B, C, H, W) image tensor with pixel values in [0, 1].
|
||||
|
||||
Returns:
|
||||
(B, D) image feature.
|
||||
"""
|
||||
@@ -559,11 +562,11 @@ class DiffusionRgbEncoder(nn.Module):
|
||||
def _replace_submodules(
|
||||
root_module: nn.Module, predicate: Callable[[nn.Module], bool], func: Callable[[nn.Module], nn.Module]
|
||||
) -> nn.Module:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
root_module: The module for which the submodules need to be replaced
|
||||
predicate: Takes a module as an argument and must return True if the that module is to be replaced.
|
||||
func: Takes a module as an argument and returns a new module to replace it with.
|
||||
|
||||
Returns:
|
||||
The root module with its submodules replaced.
|
||||
"""
|
||||
@@ -705,12 +708,12 @@ class DiffusionConditionalUnet1d(nn.Module):
|
||||
)
|
||||
|
||||
def forward(self, x: Tensor, timestep: Tensor | int, global_cond=None) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: (B, T, input_dim) tensor for input to the Unet.
|
||||
timestep: (B,) tensor of (timestep_we_are_denoising_from - 1).
|
||||
global_cond: (B, global_cond_dim)
|
||||
output: (B, T, input_dim)
|
||||
|
||||
Returns:
|
||||
(B, T, input_dim) diffusion model prediction.
|
||||
"""
|
||||
@@ -795,10 +798,10 @@ class DiffusionConditionalResidualBlock1d(nn.Module):
|
||||
)
|
||||
|
||||
def forward(self, x: Tensor, cond: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: (B, in_channels, T)
|
||||
cond: (B, cond_dim)
|
||||
|
||||
Returns:
|
||||
(B, out_channels, T)
|
||||
"""
|
||||
|
||||
@@ -34,7 +34,8 @@ def make_diffusion_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for a diffusion policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for a diffusion policy.
|
||||
|
||||
The pre-processing pipeline prepares the input data for the model by:
|
||||
1. Renaming features.
|
||||
@@ -47,8 +48,10 @@ def make_diffusion_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`DiffusionConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the diffusion policy,
|
||||
containing feature definitions, normalization mappings, and device information.
|
||||
dataset_stats: A dictionary of statistics used for normalization.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
|
||||
@@ -42,117 +42,7 @@ else:
|
||||
@PreTrainedConfig.register_subclass("eo1")
|
||||
@dataclass
|
||||
class EO1Config(PreTrainedConfig):
|
||||
"""Configuration for native EO1 policy integration in LeRobot.
|
||||
|
||||
EO1 wraps a Qwen2.5-VL vision-language backbone with a flow-matching action head: the backbone attends
|
||||
over interleaved vision/language/state/action tokens, and the head denoises an action chunk from noise
|
||||
via Euler integration.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy.
|
||||
input_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Input feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
output_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Output feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
device (`str`, *optional*):
|
||||
Torch device to run the policy on, e.g. `"cuda"` or `"cpu"`. Auto-selected when unset or
|
||||
unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str`, *optional*):
|
||||
Hub repository id to push the policy to.
|
||||
private (`bool`, *optional*):
|
||||
Whether the pushed Hub repository is private.
|
||||
tags (`list[str]`, *optional*):
|
||||
Tags to attach to the policy on the Hub.
|
||||
license (`str`, *optional*):
|
||||
License identifier for the policy on the Hub.
|
||||
pretrained_path (`Path`, *optional*):
|
||||
Repo id or local directory of pretrained weights saved with `save_pretrained`. Left unset to
|
||||
initialize from scratch.
|
||||
pretrained_revision (`str`, *optional*):
|
||||
Hub revision to pin when loading `pretrained_path`.
|
||||
vlm_base (`str`, *optional*, defaults to `"Qwen/Qwen2.5-VL-3B-Instruct"`):
|
||||
Hugging Face Hub id of the Qwen2.5-VL backbone used to initialize the vision-language model.
|
||||
vlm_config (`dict`, *optional*):
|
||||
Serialized Qwen2.5-VL backbone config. Populated automatically from `vlm_base` in
|
||||
`__post_init__` when left unset.
|
||||
image_min_pixels (`int`, *optional*, defaults to 50176):
|
||||
Minimum number of pixels the vision processor resizes an image down to.
|
||||
image_max_pixels (`int`, *optional*, defaults to 100352):
|
||||
Maximum number of pixels the vision processor resizes an image up to.
|
||||
use_fast_processor (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use the Hugging Face "fast" image processor.
|
||||
chunk_size (`int`, *optional*, defaults to 8):
|
||||
Number of actions predicted per flow-matching sampling call.
|
||||
n_action_steps (`int`, *optional*, defaults to 8):
|
||||
Number of actions from a predicted chunk that are actually executed before re-querying the
|
||||
policy. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Padded dimensionality of the state vector fed to the flow-matching head.
|
||||
max_action_dim (`int`, *optional*, defaults to 32):
|
||||
Padded dimensionality of the action vector fed to the flow-matching head.
|
||||
num_denoise_steps (`int`, *optional*, defaults to 10):
|
||||
Number of Euler integration steps used to sample an action chunk.
|
||||
num_action_layers (`int`, *optional*, defaults to 2):
|
||||
Number of linear layers in the action output projector MLP.
|
||||
action_act (`str`, *optional*, defaults to `"linear"`):
|
||||
Activation used between the action output projector's layers.
|
||||
time_sampling_beta_alpha (`float`, *optional*, defaults to 1.5):
|
||||
Alpha parameter of the Beta distribution used to sample the flow-matching timestep during
|
||||
training.
|
||||
time_sampling_beta_beta (`float`, *optional*, defaults to 1.0):
|
||||
Beta parameter of the same Beta distribution.
|
||||
time_sampling_scale (`float`, *optional*, defaults to 0.999):
|
||||
Scale applied to the sampled Beta timestep.
|
||||
time_sampling_offset (`float`, *optional*, defaults to 0.001):
|
||||
Offset added to the scaled Beta timestep.
|
||||
min_period (`float`, *optional*, defaults to 0.004):
|
||||
Minimum period of the sinusoidal timestep embedding.
|
||||
max_period (`float`, *optional*, defaults to 4.0):
|
||||
Maximum period of the sinusoidal timestep embedding.
|
||||
supervise_padding_action_dims (`bool`, *optional*, defaults to `True`):
|
||||
Whether the flow-matching loss also supervises the padded action dimensions that lie beyond
|
||||
the dataset's real action size.
|
||||
supervise_padding_actions (`bool`, *optional*, defaults to `True`):
|
||||
Whether the flow-matching loss also supervises padded action timesteps. Padded timesteps are
|
||||
marked by `action_is_pad`.
|
||||
dtype (`str`, *optional*, defaults to `"auto"`):
|
||||
Dtype requested for the Qwen backbone. `"auto"` follows the backbone checkpoint's default
|
||||
dtype (bf16 for Qwen2.5-VL); the flow-matching head always keeps its own parameters in fp32
|
||||
regardless. Other supported values are `"bfloat16"` and `"float32"`.
|
||||
force_fp32_autocast (`bool`, *optional*, defaults to `True`):
|
||||
Whether to disable autocast around the flow-matching head so its projections run in fp32 even
|
||||
when the backbone runs under bf16 autocast.
|
||||
attn_implementation (`str`, *optional*):
|
||||
Attention backend requested for the Qwen backbone, e.g. `"sdpa"` or `"flash_attention_2"`.
|
||||
Left unset to use the backbone's default.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to enable gradient checkpointing on the Qwen backbone to reduce memory usage.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps each `FeatureType` to the `NormalizationMode` used to normalize/unnormalize it.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Peak learning rate used to build the default `AdamWConfig` optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.999)`):
|
||||
Adam beta coefficients for the default optimizer preset.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Adam epsilon for the default optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.1):
|
||||
Weight decay for the default optimizer preset.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0):
|
||||
Gradient-norm clipping threshold for the default optimizer preset.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 900):
|
||||
Number of warmup steps for the default cosine-decay-with-warmup scheduler preset.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the default scheduler preset.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 0.0):
|
||||
Learning rate reached at the end of the default scheduler's decay.
|
||||
"""
|
||||
"""Configuration for native EO1 policy integration in LeRobot."""
|
||||
|
||||
vlm_base: str = "Qwen/Qwen2.5-VL-3B-Instruct"
|
||||
vlm_config: dict | None = None
|
||||
@@ -222,7 +112,6 @@ class EO1Config(PreTrainedConfig):
|
||||
scheduler_decay_lr: float = 0.0
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the VLM backbone/tokenizer configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
if self.n_action_steps > self.chunk_size:
|
||||
@@ -237,7 +126,6 @@ class EO1Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def vlm_backbone_config(self) -> Qwen2_5_VLConfig:
|
||||
"""Build the Qwen2.5-VL backbone config from `vlm_config`, applying `attn_implementation` if set."""
|
||||
require_package("transformers", extra="eo1")
|
||||
config_dict = deepcopy(self.vlm_config)
|
||||
if self.attn_implementation is not None:
|
||||
@@ -246,12 +134,10 @@ class EO1Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def text_config(self) -> Qwen2_5_VLTextConfig:
|
||||
"""The text-tower sub-config of `vlm_backbone_config`."""
|
||||
return self.vlm_backbone_config.text_config
|
||||
|
||||
@property
|
||||
def vision_config(self) -> Qwen2_5_VLVisionConfig:
|
||||
"""The vision-tower sub-config of `vlm_backbone_config`."""
|
||||
return self.vlm_backbone_config.vision_config
|
||||
|
||||
def validate_features(self) -> None:
|
||||
@@ -278,7 +164,6 @@ class EO1Config(PreTrainedConfig):
|
||||
self.output_features[ACTION] = action_feature
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -288,7 +173,6 @@ class EO1Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -298,15 +182,12 @@ class EO1Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -54,14 +54,6 @@ class EO1Policy(PreTrainedPolicy):
|
||||
name = "eo1"
|
||||
|
||||
def __init__(self, config: EO1Config, **kwargs):
|
||||
"""Build the Qwen2.5-VL backbone and the flow-matching action head.
|
||||
|
||||
Args:
|
||||
config (`EO1Config`):
|
||||
Policy configuration. Also drives whether the Qwen backbone is loaded from
|
||||
`config.vlm_base` (fresh initialization) or reconstructed from `config.vlm_backbone_config`
|
||||
(resuming from `config.pretrained_path`).
|
||||
"""
|
||||
require_package("transformers", extra="eo1")
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -88,7 +80,6 @@ class EO1Policy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue used by `select_action`."""
|
||||
self._action_queue = deque(maxlen=self.config.n_action_steps)
|
||||
|
||||
@staticmethod
|
||||
@@ -96,11 +87,6 @@ class EO1Policy(PreTrainedPolicy):
|
||||
return {key: value for key, value in batch.items() if key not in excluded_keys}
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Computes the flow-matching loss: the mean squared error between the noise-minus-action target and
|
||||
the velocity predicted by the Qwen backbone plus flow head at a sampled timestep.
|
||||
"""
|
||||
state = self.prepare_state(batch[OBS_STATE])
|
||||
actions = self.prepare_action(batch[ACTION])
|
||||
model_inputs = self._get_model_inputs(batch, {OBS_STATE, ACTION})
|
||||
@@ -111,11 +97,6 @@ class EO1Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Samples the chunk by Euler-integrating the flow-matching head from noise, then slices it back down
|
||||
to the dataset's real action dimensionality (undoing the `max_action_dim` padding).
|
||||
"""
|
||||
self.eval()
|
||||
|
||||
states = self.prepare_state(batch[OBS_STATE])
|
||||
@@ -126,16 +107,13 @@ class EO1Policy(PreTrainedPolicy):
|
||||
return actions[:, :, :original_action_dim]
|
||||
|
||||
def prepare_state(self, state: Tensor) -> Tensor:
|
||||
"""Zero-pad a state tensor up to `config.max_state_dim` for the flow-matching head."""
|
||||
return pad_vector(state, self.config.max_state_dim)
|
||||
|
||||
def prepare_action(self, action: Tensor) -> Tensor:
|
||||
"""Zero-pad an action tensor up to `config.max_action_dim` for the flow-matching head."""
|
||||
return pad_vector(action, self.config.max_action_dim)
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Uses an action queue populated by `predict_action_chunk`."""
|
||||
self.eval()
|
||||
|
||||
if len(self._action_queue) == 0:
|
||||
@@ -145,7 +123,6 @@ class EO1Policy(PreTrainedPolicy):
|
||||
return self._action_queue.popleft()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]. Trains every policy parameter with a single learning rate."""
|
||||
return self.parameters()
|
||||
|
||||
|
||||
@@ -381,6 +358,7 @@ class EO1VisionFlowMatchingModel(nn.Module):
|
||||
**kwargs,
|
||||
) -> Tensor:
|
||||
"""Run the EO1 training forward pass and compute the flow-matching loss."""
|
||||
|
||||
# 1. Build the EO1 prefix with state placeholders resolved.
|
||||
inputs_embeds = self.embed_prefix(
|
||||
input_ids,
|
||||
|
||||
@@ -31,155 +31,6 @@ logger = logging.getLogger(__name__)
|
||||
@PreTrainedConfig.register_subclass("evo1")
|
||||
@dataclass
|
||||
class Evo1Config(PreTrainedConfig):
|
||||
"""Configuration for the EVO1 vision-language-action policy.
|
||||
|
||||
EVO1 pairs an InternVL3 vision-language backbone with a flow-matching action head. Training proceeds
|
||||
in two stages (`training_stage`): stage 1 freezes the VLM and trains only the action head, stage 2
|
||||
fine-tunes the whole model.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy.
|
||||
input_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Input feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
output_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Output feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
device (`str`, *optional*):
|
||||
Torch device to run the policy on, e.g. `"cuda"` or `"cpu"`. Auto-selected when unset or
|
||||
unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use Automatic Mixed Precision. EVO1 also manages its own bfloat16 autocast around
|
||||
its forward passes independently of this flag; see `dtype`-related fields below.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str`, *optional*):
|
||||
Hub repository id to push the policy to.
|
||||
private (`bool`, *optional*):
|
||||
Whether the pushed Hub repository is private.
|
||||
tags (`list[str]`, *optional*):
|
||||
Tags to attach to the policy on the Hub.
|
||||
license (`str`, *optional*):
|
||||
License identifier for the policy on the Hub.
|
||||
pretrained_path (`Path`, *optional*):
|
||||
Repo id or local directory of pretrained weights saved with `save_pretrained`. Left unset to
|
||||
initialize from scratch.
|
||||
pretrained_revision (`str`, *optional*):
|
||||
Hub revision to pin when loading `pretrained_path`.
|
||||
training_stage (`str`, *optional*, defaults to `"stage1"`):
|
||||
Either `"stage1"` (VLM frozen, only the action head trains) or `"stage2"` (the whole model
|
||||
trains). Drives the default `finetune_*` flags unless they are set explicitly and
|
||||
`apply_training_stage_defaults` is `False`.
|
||||
chunk_size (`int`, *optional*, defaults to 50):
|
||||
Number of actions predicted by the flow-matching head per inference call.
|
||||
n_action_steps (`int`, *optional*, defaults to 50):
|
||||
Number of actions from a predicted chunk that are actually executed before re-querying the
|
||||
policy. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 24):
|
||||
Padded dimensionality of the state vector fed to the action head.
|
||||
max_action_dim (`int`, *optional*, defaults to 24):
|
||||
Padded dimensionality of the action vector fed to the action head.
|
||||
max_views (`int`, *optional*, defaults to 3):
|
||||
Maximum number of camera streams the policy accepts.
|
||||
image_resolution (`tuple[int, int]`, *optional*, defaults to `(448, 448)`):
|
||||
Target resolution images are resized to before the InternVL3 embedder. Must be square.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of placeholder, always-masked-out camera views added to `input_features` so the batch
|
||||
has a fixed number of views regardless of how many real cameras the dataset provides.
|
||||
postprocess_action_dim (`int`, *optional*):
|
||||
Overrides the action dimensionality the postprocessor crops predictions down to. Falls back to
|
||||
the dataset's action feature width, or `max_action_dim` if that is unavailable.
|
||||
binarize_gripper (`bool`, *optional*, defaults to `False`):
|
||||
Whether the postprocessor snaps the gripper action channel to one of two fixed values instead
|
||||
of passing through the continuous prediction.
|
||||
gripper_index (`int`, *optional*, defaults to 6):
|
||||
Index of the gripper channel within the action vector, used when `binarize_gripper` is `True`.
|
||||
gripper_threshold (`float`, *optional*, defaults to 0.5):
|
||||
Decision threshold applied to the gripper channel when `binarize_gripper` is `True`.
|
||||
gripper_below_threshold_value (`float`, *optional*, defaults to 1.0):
|
||||
Value written to the gripper channel when it is at or below `gripper_threshold`.
|
||||
gripper_above_threshold_value (`float`, *optional*, defaults to -1.0):
|
||||
Value written to the gripper channel when it is above `gripper_threshold`.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps each `FeatureType` to the `NormalizationMode` used to normalize/unnormalize it.
|
||||
vlm_model_name (`str`, *optional*, defaults to `"OpenGVLab/InternVL3-1B-hf"`):
|
||||
Hugging Face Hub id of the InternVL3 vision-language backbone.
|
||||
vlm_num_layers (`int`, *optional*, defaults to 14):
|
||||
Number of transformer layers kept from the InternVL3 language model. `None` keeps all of them.
|
||||
vlm_dtype (`str`, *optional*, defaults to `"bfloat16"`):
|
||||
Dtype the InternVL3 backbone is loaded in.
|
||||
max_text_length (`int`, *optional*, defaults to 1024):
|
||||
Maximum token length for the tokenized (image placeholders + instruction) prompt. Longer
|
||||
prompts are right-truncated.
|
||||
use_flash_attn (`bool`, *optional*, defaults to `True`):
|
||||
Whether to request FlashAttention in the InternVL3 backbone.
|
||||
action_head (`str`, *optional*, defaults to `"flowmatching"`):
|
||||
Identifier of the action-generation head architecture.
|
||||
embed_dim (`int`, *optional*, defaults to 896):
|
||||
Dimensionality of the fused vision-language token embeddings consumed by the action head.
|
||||
hidden_dim (`int`, *optional*, defaults to 1024):
|
||||
Hidden width of the action head's transformer layers.
|
||||
state_hidden_dim (`int`, *optional*, defaults to 1024):
|
||||
Hidden width of the state encoder inside the action head.
|
||||
num_heads (`int`, *optional*, defaults to 8):
|
||||
Number of attention heads in the action head's transformer layers.
|
||||
num_layers (`int`, *optional*, defaults to 8):
|
||||
Number of transformer layers in the action head.
|
||||
dropout (`float`, *optional*, defaults to 0.0):
|
||||
Dropout probability applied inside the action head.
|
||||
num_inference_timesteps (`int`, *optional*, defaults to 32):
|
||||
Number of integration steps used to sample an action chunk from the flow-matching head.
|
||||
num_categories (`int`, *optional*, defaults to 1):
|
||||
Number of embodiment categories the action head conditions on.
|
||||
return_cls_only (`bool`, *optional*, defaults to `False`):
|
||||
Whether the action head is conditioned on a single pooled VL token (the last non-padding token
|
||||
of the causal decoder) instead of the full fused token sequence.
|
||||
enable_gradient_checkpointing (`bool`, *optional*, defaults to `True`):
|
||||
Whether to enable gradient checkpointing on the VLM backbone to reduce memory usage.
|
||||
gradient_checkpointing_use_reentrant (`bool`, *optional*, defaults to `False`):
|
||||
Whether gradient checkpointing uses the reentrant autograd variant.
|
||||
finetune_vlm (`bool`, *optional*):
|
||||
Whether the whole VLM backbone is trainable. Defaulted from `training_stage` unless set
|
||||
explicitly with `apply_training_stage_defaults=False`. Must agree with the union of
|
||||
`finetune_language_model` and `finetune_vision_model` when those are set explicitly.
|
||||
finetune_language_model (`bool`, *optional*):
|
||||
Whether the VLM's language branch is trainable. Defaulted from `training_stage` unless set
|
||||
explicitly with `apply_training_stage_defaults=False`.
|
||||
finetune_vision_model (`bool`, *optional*):
|
||||
Whether the VLM's vision branch is trainable. Defaulted from `training_stage` unless set
|
||||
explicitly with `apply_training_stage_defaults=False`.
|
||||
finetune_action_head (`bool`, *optional*):
|
||||
Whether the flow-matching action head is trainable. Defaulted from `training_stage` unless set
|
||||
explicitly with `apply_training_stage_defaults=False`.
|
||||
apply_training_stage_defaults (`bool`, *optional*, defaults to `True`):
|
||||
Whether to reapply the `training_stage` defaults to the `finetune_*` flags after loading a
|
||||
checkpoint config, so a stage-2 run cannot silently inherit a stage-1 checkpoint's frozen-VLM
|
||||
flags. Set `False` to keep explicit finetuning flags.
|
||||
task_field (`str`, *optional*, defaults to `"task"`):
|
||||
Batch key holding the language instruction(s) passed to the VLM.
|
||||
embodiment_id_field (`str`, *optional*):
|
||||
Batch key holding an explicit per-sample embodiment id. Falls back to `"embodiment_id"`, then
|
||||
to `default_embodiment_id`, when unset or absent from the batch.
|
||||
default_embodiment_id (`int`, *optional*, defaults to 0):
|
||||
Embodiment id used when the batch carries none. Must be in `[0, num_categories)`.
|
||||
rtc_config (`RTCConfig`, *optional*):
|
||||
Real-Time Chunking guidance for asynchronous inference. `None` disables RTC.
|
||||
`lerobot-rollout --inference.type=rtc` sets this and calls `init_rtc_processor()`.
|
||||
optimizer_lr (`float`, *optional*, defaults to 1e-05):
|
||||
Learning rate used to build the default `AdamWConfig` optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.999)`):
|
||||
Adam beta coefficients for the default optimizer preset.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Adam epsilon for the default optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-05):
|
||||
Weight decay applied to the decayed parameter group in the default optimizer preset.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0):
|
||||
Gradient-norm clipping threshold for the default optimizer preset.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 300):
|
||||
Number of warmup steps for the default cosine-annealing-with-warmup scheduler preset.
|
||||
"""
|
||||
|
||||
training_stage: str = "stage1"
|
||||
# When True and the policy runs on CUDA, EVO1 wraps its own forward passes (training and
|
||||
# inference) in a bfloat16 autocast block, so its numerics do not depend on the dtype of any
|
||||
@@ -257,7 +108,6 @@ class Evo1Config(PreTrainedConfig):
|
||||
scheduler_warmup_steps: int = 300
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the VLM backbone/tokenizer configuration."""
|
||||
super().__post_init__()
|
||||
if self.training_stage not in {"stage1", "stage2"}:
|
||||
raise ValueError(
|
||||
@@ -350,7 +200,6 @@ class Evo1Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if self.input_features is None:
|
||||
self.input_features = {}
|
||||
if self.output_features is None:
|
||||
@@ -377,7 +226,6 @@ class Evo1Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -387,22 +235,18 @@ class Evo1Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineAnnealingWithWarmupSchedulerConfig(
|
||||
num_warmup_steps=self.scheduler_warmup_steps,
|
||||
)
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return [0]
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -33,43 +33,19 @@ from .evo1_model import Evo1Model
|
||||
|
||||
|
||||
class ActionSelectKwargs(TypedDict, total=False):
|
||||
"""Extra keyword arguments accepted by EVO1's `select_action`/`predict_action_chunk` for RTC inference.
|
||||
|
||||
**Attributes**:
|
||||
- **inference_delay** (`int | None`) -- Number of environment steps the previous inference call
|
||||
took, used by the RTC processor to blend the new chunk with `prev_chunk_left_over`.
|
||||
- **prev_chunk_left_over** (`Tensor | None`) -- Unconsumed tail of the previously predicted action
|
||||
chunk, blended with the new prediction for a smooth handoff.
|
||||
- **execution_horizon** (`int | None`) -- Number of steps of the new chunk that will actually be
|
||||
executed before the next inference call, used to weight the RTC blend.
|
||||
"""
|
||||
|
||||
inference_delay: int | None
|
||||
prev_chunk_left_over: Tensor | None
|
||||
execution_horizon: int | None
|
||||
|
||||
|
||||
class Evo1Policy(PreTrainedPolicy):
|
||||
"""EVO1 vision-language-action policy: an InternVL3 backbone with a flow-matching action head."""
|
||||
|
||||
config_class = Evo1Config
|
||||
name = "evo1"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`]. EVO1 supports Real-Time Chunking."""
|
||||
return True
|
||||
|
||||
def __init__(self, config: Evo1Config, *, vlm_hub_kwargs: dict | None = None, **kwargs):
|
||||
"""Build the InternVL3 vision-language embedder and the flow-matching action head.
|
||||
|
||||
Args:
|
||||
config (`Evo1Config`):
|
||||
Policy configuration.
|
||||
vlm_hub_kwargs (`dict`, *optional*):
|
||||
Hub download options (`token`, `cache_dir`, `local_files_only`, `proxies`) forwarded to the
|
||||
VLM backbone's own `from_pretrained` call, as distinct from the ones used to load this
|
||||
policy's own checkpoint.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
|
||||
@@ -117,12 +93,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
strict: bool | None = None,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`].
|
||||
|
||||
Defaults `strict` to `True` instead of `False`, and additionally forwards `vlm_hub_kwargs` (or
|
||||
derives them from `token`, `cache_dir`, `local_files_only`, and `proxies`) to the InternVL3
|
||||
backbone's own `from_pretrained` call.
|
||||
"""
|
||||
if strict is None:
|
||||
strict = True
|
||||
vlm_hub_kwargs = kwargs.pop("vlm_hub_kwargs", None)
|
||||
@@ -200,11 +170,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
return nullcontext()
|
||||
|
||||
def get_optim_params(self) -> list[dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Splits parameters into a weight-decayed group and a no-decay group (biases and 1D/normalization
|
||||
parameters).
|
||||
"""
|
||||
decay, no_decay = [], []
|
||||
for name, param in self.named_parameters():
|
||||
if not param.requires_grad:
|
||||
@@ -221,7 +186,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
]
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue used by `select_action`."""
|
||||
self._action_queue = deque([], maxlen=self.config.n_action_steps)
|
||||
|
||||
def _normalize_task_batch(self, batch: dict[str, Tensor | list[str] | str]) -> list[str]:
|
||||
@@ -398,12 +362,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
embedder.eval()
|
||||
|
||||
def train(self, mode: bool = True):
|
||||
"""Set training mode, keeping the VLM embedder in eval mode when its weights are frozen.
|
||||
|
||||
Args:
|
||||
mode (`bool`, *optional*, defaults to `True`):
|
||||
Whether to set training (`True`) or evaluation (`False`) mode.
|
||||
"""
|
||||
super().train(mode)
|
||||
self._keep_frozen_embedder_eval()
|
||||
return self
|
||||
@@ -494,12 +452,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
return sq_error.sum() / active.sum()
|
||||
|
||||
def forward(self, batch: dict[str, Tensor], reduction: str = "mean") -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Computes the flow-matching velocity-regression loss (squared error between the predicted and
|
||||
target velocity), masked to the active state/action dimensions and averaged per sample. Set
|
||||
`reduction="none"` to get the per-sample loss instead of the batch mean.
|
||||
"""
|
||||
prompts = self._normalize_task_batch(batch)
|
||||
image_batches, image_masks = self._collect_image_batches(batch)
|
||||
states, _state_mask = self._prepare_state(batch)
|
||||
@@ -534,12 +486,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Accepts `ActionSelectKwargs`'s RTC-specific arguments (`inference_delay`, `prev_chunk_left_over`,
|
||||
`execution_horizon`), which are rejected unless `config.rtc_config` is set and
|
||||
`init_rtc_processor()` has been called.
|
||||
"""
|
||||
inference_delay = kwargs.get("inference_delay")
|
||||
prev_chunk_left_over = kwargs.get("prev_chunk_left_over")
|
||||
execution_horizon = kwargs.get("execution_horizon")
|
||||
@@ -576,11 +522,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`. Real-Time Chunking is not supported
|
||||
here; use `predict_action_chunk` directly when `config.rtc_config` is enabled.
|
||||
"""
|
||||
assert not self._rtc_enabled(), (
|
||||
"RTC is not supported for select_action, use it with predict_action_chunk"
|
||||
)
|
||||
|
||||
@@ -381,25 +381,6 @@ def make_evo1_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Build the pre/post-processor pipelines for EVO1.
|
||||
|
||||
The preprocessor pads observation state and training actions to EVO1's fixed `max_state_dim` /
|
||||
`max_action_dim` widths (tracking the padding with an `action_mask`) before normalizing and moving the
|
||||
batch to `config.device`. The postprocessor unnormalizes predicted actions, crops them back down to the
|
||||
real action dimensionality, optionally binarizes the gripper channel, and moves the result to CPU.
|
||||
|
||||
Args:
|
||||
config (`Evo1Config`):
|
||||
EVO1 policy configuration.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]]`, *optional*):
|
||||
Per-feature normalization statistics, as produced by `LeRobotDatasetMetadata.stats`. Padded to
|
||||
`max_state_dim`/`max_action_dim` before being handed to the (un)normalizer steps.
|
||||
|
||||
Returns:
|
||||
`tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]`: The preprocessor (batch of raw
|
||||
observations/actions -> model input) and postprocessor (model output -> environment action)
|
||||
pipelines.
|
||||
"""
|
||||
normalization_features = _evo1_normalization_features(config)
|
||||
action_features = _evo1_action_features(config)
|
||||
normalization_stats = _pad_evo1_stats(config, dataset_stats)
|
||||
|
||||
@@ -77,7 +77,8 @@ def _reconnect_relative_absolute_steps(
|
||||
|
||||
|
||||
def get_policy_class(name: str) -> type[PreTrainedPolicy]:
|
||||
"""Retrieves a policy class by its registered name.
|
||||
"""
|
||||
Retrieves a policy class by its registered name.
|
||||
|
||||
Resolution is convention-based: the draccus-registered config class of ``name`` is
|
||||
looked up, its ``configuration_*`` module path is rewritten to ``modeling_*``, and
|
||||
@@ -87,8 +88,7 @@ def get_policy_class(name: str) -> type[PreTrainedPolicy]:
|
||||
``@PreTrainedConfig.register_subclass``).
|
||||
|
||||
Args:
|
||||
name (`str`): The registered name of the policy (e.g. "act", "diffusion", "pi0").
|
||||
|
||||
name: The registered name of the policy (e.g. "act", "diffusion", "pi0").
|
||||
Returns:
|
||||
The policy class corresponding to the given name.
|
||||
|
||||
@@ -100,15 +100,16 @@ def get_policy_class(name: str) -> type[PreTrainedPolicy]:
|
||||
|
||||
|
||||
def make_policy_config(policy_type: str, **kwargs) -> PreTrainedConfig:
|
||||
"""Instantiates a policy configuration object based on the policy type.
|
||||
"""
|
||||
Instantiates a policy configuration object based on the policy type.
|
||||
|
||||
This factory function simplifies the creation of policy configuration objects by
|
||||
mapping a string identifier to the corresponding config class.
|
||||
|
||||
Args:
|
||||
policy_type (`str`): The registered type of the policy (any name registered via
|
||||
`@PreTrainedConfig.register_subclass`, e.g. "act", "diffusion", "pi0").
|
||||
kwargs (`Any`, *optional*): Keyword arguments to be passed to the configuration class constructor.
|
||||
policy_type: The registered type of the policy (any name registered via
|
||||
``@PreTrainedConfig.register_subclass``, e.g. "act", "diffusion", "pi0").
|
||||
**kwargs: Keyword arguments to be passed to the configuration class constructor.
|
||||
|
||||
Returns:
|
||||
An instance of a `PreTrainedConfig` subclass.
|
||||
@@ -124,7 +125,8 @@ def make_policy_config(policy_type: str, **kwargs) -> PreTrainedConfig:
|
||||
|
||||
|
||||
class ProcessorConfigKwargs(TypedDict, total=False):
|
||||
"""A TypedDict defining the keyword arguments for processor configuration.
|
||||
"""
|
||||
A TypedDict defining the keyword arguments for processor configuration.
|
||||
|
||||
This provides type hints for the optional arguments passed to `make_pre_post_processors`,
|
||||
improving code clarity and enabling static analysis.
|
||||
@@ -158,7 +160,8 @@ def make_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Create or load pre- and post-processor pipelines for a given policy.
|
||||
"""
|
||||
Create or load pre- and post-processor pipelines for a given policy.
|
||||
|
||||
This function acts as a factory. It can either load existing processor pipelines
|
||||
from a pretrained path or create new ones from scratch based on the policy
|
||||
@@ -169,7 +172,6 @@ def make_pre_post_processors(
|
||||
policy_cfg: The configuration of the policy for which to create processors.
|
||||
pretrained_path: An optional path to load pretrained processor pipelines from.
|
||||
If provided, pipelines are loaded from this path.
|
||||
pretrained_revision: The Hub revision to load `pretrained_path` from, if it's a Hub repo id.
|
||||
**kwargs: Keyword arguments for processor configuration, as defined in
|
||||
`ProcessorConfigKwargs`.
|
||||
|
||||
@@ -246,7 +248,8 @@ def make_policy(
|
||||
rename_map: dict[str, str] | None = None,
|
||||
defer_weight_load: bool = False,
|
||||
) -> PreTrainedPolicy:
|
||||
"""Instantiate a policy model.
|
||||
"""
|
||||
Instantiate a policy model.
|
||||
|
||||
This factory function handles the logic of creating a policy, which requires
|
||||
determining the input and output feature shapes. These shapes can be derived
|
||||
@@ -256,13 +259,13 @@ def make_policy(
|
||||
Args:
|
||||
cfg (PreTrainedConfig): The configuration for the policy to be created. If
|
||||
`cfg.pretrained_path` is set, the policy will be loaded with weights from that path.
|
||||
ds_meta (LeRobotDatasetMetadata | None, *optional*): Dataset metadata used to infer feature shapes and
|
||||
ds_meta (LeRobotDatasetMetadata | None): Dataset metadata used to infer feature shapes and
|
||||
types. Also provides statistics for normalization layers.
|
||||
env_cfg (EnvConfig | None, *optional*): Environment configuration used to infer feature shapes and
|
||||
env_cfg (EnvConfig | None): Environment configuration used to infer feature shapes and
|
||||
types. One of `ds_meta` or `env_cfg` must be provided.
|
||||
rename_map (dict[str, str] | None, *optional*): Optional mapping of dataset or environment feature
|
||||
rename_map (dict[str, str] | None): Optional mapping of dataset or environment feature
|
||||
keys to match expected policy feature names (e.g., `"left"` → `"camera1"`).
|
||||
defer_weight_load (bool, *optional*, defaults to `False`): Build the exact policy `from_pretrained` would build — same
|
||||
defer_weight_load (bool): Build the exact policy `from_pretrained` would build — same
|
||||
config resolution, same stats-derived buffers, same device placement and eval mode —
|
||||
but skip the safetensors weight load. Used when resuming from a DCP checkpoint, whose
|
||||
sharded weights stream in after `accelerator.prepare()` (the distributed checkpoint
|
||||
@@ -409,7 +412,6 @@ def _get_policy_cls_from_policy_name(name: str) -> type[PreTrainedPolicy]:
|
||||
|
||||
Args:
|
||||
name: The name of the policy.
|
||||
|
||||
Returns:
|
||||
The policy class corresponding to the given name.
|
||||
"""
|
||||
@@ -465,10 +467,10 @@ def _make_processors_from_policy_config(
|
||||
dataset_stats: Dataset statistics for normalization.
|
||||
dataset_meta: Dataset metadata, forwarded only to factories that declare a
|
||||
``dataset_meta`` parameter (e.g. groot, molmoact2).
|
||||
|
||||
Returns:
|
||||
A tuple containing the input (pre-processor) and output (post-processor) pipelines.
|
||||
"""
|
||||
|
||||
policy_type = config.type
|
||||
function_name = f"make_{policy_type}_pre_post_processors"
|
||||
module_path = config.__class__.__module__.replace(
|
||||
|
||||
@@ -58,7 +58,6 @@ _FASTWAM_ACTION_BASE_COMPAT_KEYS = (
|
||||
|
||||
|
||||
def default_video_dit_config(action_dim: int) -> dict[str, Any]:
|
||||
"""Return the default kwargs dict for the video-generation DiT backbone, sized for `action_dim`."""
|
||||
return {
|
||||
"patch_size": [1, 2, 2],
|
||||
"in_dim": 48,
|
||||
@@ -82,7 +81,6 @@ def default_video_dit_config(action_dim: int) -> dict[str, Any]:
|
||||
|
||||
|
||||
def default_action_dit_config(action_dim: int) -> dict[str, Any]:
|
||||
"""Return the default kwargs dict for the action-generation DiT backbone, sized for `action_dim`."""
|
||||
return {
|
||||
"action_dim": action_dim,
|
||||
"hidden_dim": 1024,
|
||||
@@ -138,6 +136,7 @@ def _validate_wan_model_id(value: str, field_name: str) -> str:
|
||||
|
||||
def is_fastwam_base_compatible_config(config: FastWAMConfig) -> bool:
|
||||
"""Return whether `fastwam_base` partial weights can initialize this config."""
|
||||
|
||||
default_video_config = default_video_dit_config(config.action_dim)
|
||||
default_action_config = default_action_dit_config(config.action_dim)
|
||||
return all(
|
||||
@@ -154,129 +153,30 @@ def is_fastwam_base_compatible_config(config: FastWAMConfig) -> bool:
|
||||
class FastWAMConfig(PreTrainedConfig):
|
||||
"""Configuration for the FastWAM LeRobot policy.
|
||||
|
||||
FastWAM adapts the Wan2.2 video-diffusion backbone into a robot policy: a video expert and an action
|
||||
expert are jointly trained (or fine-tuned) as a Mixture-of-Transformers, sharing attention over a
|
||||
predicted future video and the corresponding action chunk.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy.
|
||||
input_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Input feature specification, keyed by feature name. `__post_init__` builds a synthetic
|
||||
single-image default at `image_size` when left unset; `set_dataset_feature_metadata` later
|
||||
replaces it with the dataset's real per-camera keys.
|
||||
output_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Output feature specification, keyed by feature name. `__post_init__` builds a default `action`
|
||||
feature of shape `(action_dim,)` when left unset.
|
||||
device (`str`, *optional*):
|
||||
Torch device to run the policy on, e.g. `"cuda"` or `"cpu"`. Auto-selected when unset or
|
||||
unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str`, *optional*):
|
||||
Hub repository id to push the policy to.
|
||||
private (`bool`, *optional*):
|
||||
Whether the pushed Hub repository is private.
|
||||
tags (`list[str]`, *optional*):
|
||||
Tags to attach to the policy on the Hub.
|
||||
license (`str`, *optional*):
|
||||
License identifier for the policy on the Hub.
|
||||
pretrained_path (`Path`, *optional*):
|
||||
Repo id or local directory of pretrained weights saved with `save_pretrained`. Auto-populated
|
||||
from `base_model_id` when the DiT configs are `fastwam_base`-compatible; otherwise left unset
|
||||
to initialize from scratch.
|
||||
pretrained_revision (`str`, *optional*):
|
||||
Hub revision to pin when loading `pretrained_path`.
|
||||
action_dim (`int`, *optional*, defaults to 7):
|
||||
Number of scalar action channels per timestep.
|
||||
proprio_dim (`int`, *optional*, defaults to 8):
|
||||
Number of proprioception channels used as an extra text-context token. `None` disables proprio
|
||||
conditioning.
|
||||
action_horizon (`int`, *optional*, defaults to 32):
|
||||
Number of actions predicted by one policy call.
|
||||
n_action_steps (`int`, *optional*, defaults to 32):
|
||||
Number of actions from a predicted chunk that are actually executed before re-querying the
|
||||
policy. Must not exceed `action_horizon`.
|
||||
num_video_frames (`int`, *optional*, defaults to 33):
|
||||
Raw video sampling window, in dataset frames. The model actually operates on
|
||||
`model_video_frames` frames after subsampling by `action_video_freq_ratio`.
|
||||
action_video_freq_ratio (`int`, *optional*, defaults to 4):
|
||||
Actions are sampled at this multiple of the video frame rate. Video frames are taken every
|
||||
`action_video_freq_ratio`-th raw frame, so the model sees `(num_video_frames - 1) // ratio + 1`
|
||||
frames spanning the same time window as `action_horizon` actions.
|
||||
image_size (`tuple[int, int]`, *optional*, defaults to `(224, 448)`):
|
||||
Concatenated image size as `(height, width)`, shared across every camera view.
|
||||
context_len (`int`, *optional*, defaults to 128):
|
||||
Maximum text embedding token length.
|
||||
model_id (`str`, *optional*, defaults to `"Wan-AI/Wan2.2-TI2V-5B"`):
|
||||
Hub id (or local path) of the Wan2.2 video-diffusion backbone.
|
||||
tokenizer_model_id (`str`, *optional*, defaults to `"google/umt5-xxl"`):
|
||||
Hub id of the UMT5 tokenizer.
|
||||
text_encoder_model_id (`str`, *optional*, defaults to `"Wan-AI/Wan2.2-TI2V-5B-Diffusers"`):
|
||||
Hub id of the frozen UMT5 text encoder and VAE used for text/video conditioning.
|
||||
base_model_id (`str`, *optional*, defaults to `"lerobot/fastwam_base"`):
|
||||
Hub id of the FastWAM base checkpoint used to auto-populate `pretrained_path` when the DiT
|
||||
configs are compatible with it. `None` disables this auto-loading.
|
||||
tokenizer_max_len (`int`, *optional*, defaults to 128):
|
||||
Maximum token length passed to the tokenizer.
|
||||
load_text_encoder (`bool`, *optional*, defaults to `True`):
|
||||
Whether to load the frozen UMT5 text encoder. Disable when the batch always supplies
|
||||
precomputed `context`/`context_mask`.
|
||||
mot_checkpoint_mixed_attn (`bool`, *optional*, defaults to `False`):
|
||||
Whether the Mixture-of-Transformers module checkpoints its mixed video/action attention.
|
||||
torch_dtype (`str`, *optional*, defaults to `"bfloat16"`):
|
||||
Dtype the Wan backbone and action expert are built and run in.
|
||||
prompt_template (`str`, *optional*, defaults to `"A video recorded from a robot's point of view executing the following instruction: {task}"`):
|
||||
Template the raw `task` string is formatted into before text encoding.
|
||||
num_inference_steps (`int`, *optional*, defaults to 10):
|
||||
Number of denoising steps used at inference time.
|
||||
inference_seed (`int`, *optional*, defaults to 42):
|
||||
Random seed for the inference noise sampler. `None` samples fresh noise every call.
|
||||
rand_device (`str`, *optional*, defaults to `"cpu"`):
|
||||
Device the inference noise sampler draws from.
|
||||
text_cfg_scale (`float`, *optional*, defaults to 1.0):
|
||||
Classifier-free-guidance scale applied against `negative_prompt` at inference time.
|
||||
negative_prompt (`str`, *optional*, defaults to `""`):
|
||||
Negative prompt used for classifier-free guidance.
|
||||
sigma_shift (`float`, *optional*):
|
||||
Overrides the diffusion schedule's sigma shift at inference time. `None` uses the scheduler's
|
||||
own shift.
|
||||
tiled (`bool`, *optional*, defaults to `False`):
|
||||
Whether to run the Wan VAE in tiled mode to reduce memory use.
|
||||
fp32_attention (`bool`, *optional*, defaults to `True`):
|
||||
Whether the video and action DiT experts compute attention in fp32.
|
||||
use_gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to enable activation checkpointing in both DiT experts, trading compute for memory.
|
||||
Propagated into `video_dit_config` and `action_dit_config`.
|
||||
freeze_video_expert (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the ~5B Wan video expert so only the action expert and proprio encoder
|
||||
train, cutting the AdamW optimizer footprint substantially. Also set `loss.lambda_video=0` to
|
||||
skip the now-gradient-free video loss compute.
|
||||
toggle_action_dimensions (`list[int]`, *optional*):
|
||||
Action dimensions the postprocessor flips between two fixed values, for LIBERO-style toggle
|
||||
actions such as the gripper. Empty disables the toggle.
|
||||
video_scheduler (`dict[str, float | int]`, *optional*):
|
||||
Train/inference shift and step-count settings for the video diffusion scheduler.
|
||||
action_scheduler (`dict[str, float | int]`, *optional*):
|
||||
Train/inference shift and step-count settings for the action diffusion scheduler.
|
||||
loss (`dict[str, float]`, *optional*):
|
||||
Per-term loss weights, keyed by `"lambda_video"` and `"lambda_action"`.
|
||||
video_dit_config (`dict[str, Any]`, *optional*):
|
||||
Wan video expert architecture config. Built from `default_video_dit_config(action_dim)` when
|
||||
left unset.
|
||||
action_dit_config (`dict[str, Any]`, *optional*):
|
||||
Action expert architecture config. Built from `default_action_dit_config(action_dim)` when
|
||||
left unset.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps each `FeatureType` to the `NormalizationMode` used to normalize/unnormalize it.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Learning rate used to build the default `AdamWConfig` optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.01):
|
||||
Weight decay for the default optimizer preset.
|
||||
action_dim (int): Number of scalar action channels per timestep.
|
||||
proprio_dim (int | None): Number of proprioception channels used as an
|
||||
extra text-context token. `None` disables proprio conditioning.
|
||||
action_horizon (int): Number of actions predicted by one policy call.
|
||||
num_video_frames (int): Raw video sampling window (in dataset frames). The
|
||||
model actually operates on `model_video_frames` frames after subsampling
|
||||
by `action_video_freq_ratio`.
|
||||
action_video_freq_ratio (int): Actions are sampled at this multiple of the
|
||||
video frame rate. Video frames are taken every `action_video_freq_ratio`-th
|
||||
raw frame, so the model sees `(num_video_frames - 1) // ratio + 1` frames
|
||||
spanning the same time window as `action_horizon` actions (ratio actions
|
||||
per video frame).
|
||||
image_size (tuple[int, int]): Concatenated image size as `(height, width)`.
|
||||
context_len (int): Maximum text embedding token length.
|
||||
video_dit_config (dict[str, Any] | None): Wan video expert config.
|
||||
action_dit_config (dict[str, Any] | None): Action expert config.
|
||||
use_gradient_checkpointing (bool): Enable activation checkpointing in both DiT
|
||||
experts (trades compute for memory; propagated into the DiT configs).
|
||||
freeze_video_expert (bool): Freeze the ~5B Wan video expert
|
||||
(`model.video_expert`) so only the action expert + proprio encoder train.
|
||||
Cuts the AdamW optimizer footprint substantially; the video expert keeps its
|
||||
pretrained weights. (If enabled, also set `loss.lambda_video=0` to skip the
|
||||
now-gradient-free video loss compute.)
|
||||
"""
|
||||
|
||||
n_obs_steps: int = 1
|
||||
@@ -332,7 +232,6 @@ class FastWAMConfig(PreTrainedConfig):
|
||||
optimizer_weight_decay: float = 1.0e-2
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the DiT/video backbone configuration."""
|
||||
super().__post_init__()
|
||||
self.image_size = tuple(self.image_size)
|
||||
self.model_id = _validate_wan_model_id(self.model_id, "model_id")
|
||||
@@ -381,11 +280,9 @@ class FastWAMConfig(PreTrainedConfig):
|
||||
self.pretrained_path = pretrained_path
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(lr=self.optimizer_lr, weight_decay=self.optimizer_weight_decay)
|
||||
|
||||
def get_scheduler_preset(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return None
|
||||
|
||||
def set_dataset_feature_metadata(self, dataset_features: dict[str, Any]) -> None:
|
||||
@@ -420,7 +317,6 @@ class FastWAMConfig(PreTrainedConfig):
|
||||
self.validate_features()
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if self.action_dim <= 0:
|
||||
raise ValueError(f"`action_dim` must be positive, got {self.action_dim}.")
|
||||
if self.action_horizon <= 0:
|
||||
@@ -481,16 +377,12 @@ class FastWAMConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def model_video_frames(self) -> int:
|
||||
"""Number of video frames the model actually operates on.
|
||||
|
||||
Computed by subsampling the raw `num_video_frames` window by `action_video_freq_ratio` (e.g.
|
||||
33 -> 9).
|
||||
"""
|
||||
"""Number of video frames the model actually operates on, after subsampling the
|
||||
raw `num_video_frames` window by `action_video_freq_ratio` (e.g. 33 -> 9)."""
|
||||
return (self.num_video_frames - 1) // self.action_video_freq_ratio + 1
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
# Load the video frames the model is supervised on: the future window subsampled by
|
||||
# action_video_freq_ratio (e.g. [0, 4, 8, ..., 32] -> 9 frames). Each video frame is
|
||||
# thus `action_video_freq_ratio` actions apart, while actions load at the full rate
|
||||
@@ -500,10 +392,8 @@ class FastWAMConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.action_horizon))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -45,6 +45,11 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
arbitrary boolean ``[query, key]`` masks that the FlashAttention varlen API cannot express;
|
||||
installing ``flash-attn`` has no effect on the FastWAM path. (SDPA may still dispatch to
|
||||
PyTorch's own flash/mem-efficient/math kernel internally, unrelated to the ``flash-attn`` package.)
|
||||
|
||||
Args:
|
||||
config (FastWAMConfig): FastWAM policy configuration.
|
||||
dataset_stats (dict[str, dict[str, Tensor]] | None): Optional LeRobot
|
||||
dataset statistics passed by the training/evaluation stack.
|
||||
"""
|
||||
|
||||
config_class = FastWAMConfig
|
||||
@@ -59,17 +64,6 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
dataset_stats: dict[str, dict[str, Tensor]] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Build the FastWAM core model (video expert, action expert, and MoT router).
|
||||
|
||||
Args:
|
||||
config (`FastWAMConfig`):
|
||||
FastWAM policy configuration.
|
||||
dataset_stats (`dict[str, dict[str, Tensor]]`, *optional*):
|
||||
LeRobot dataset statistics passed by the training/evaluation stack. Accepted for
|
||||
signature compatibility with other policies but not otherwise used here.
|
||||
kwargs: Additional keyword arguments (e.g. `dataset_meta`) forwarded by `make_policy` or
|
||||
`from_pretrained`; accepted and ignored.
|
||||
"""
|
||||
# FastWAM's Wan2.2 backbone needs transformers (UMT5 text encoder/tokenizer) and
|
||||
# diffusers (Wan VAE), both behind the `fastwam` extra. Fail fast with an actionable
|
||||
# message in base installs rather than deep in Wan component construction.
|
||||
@@ -146,12 +140,6 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
return model
|
||||
|
||||
def get_optim_params(self) -> list[Tensor]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Returns a flat list of trainable tensors (DiT parameters plus the proprio encoder's, when
|
||||
present) rather than a param-group dict, so parameters frozen via `freeze_video_expert` are
|
||||
excluded.
|
||||
"""
|
||||
# Return the trainable tensors directly (a single param group). The optimizer
|
||||
# builder wraps these in a param group; returning a bare {"params": [...]} dict
|
||||
# instead would make `list(...)` yield the key string "params".
|
||||
@@ -164,7 +152,6 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
return [p for p in params if p.requires_grad]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue used by `select_action`."""
|
||||
self._action_queue: deque[Tensor] = deque([], maxlen=self.config.n_action_steps)
|
||||
|
||||
def _batch_to_training_sample(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
|
||||
@@ -200,24 +187,36 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
return sample
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict[str, Any]]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
"""Compute FastWAM training loss for a LeRobot batch.
|
||||
|
||||
Accepts either FastWAM-native batch keys (`video`, `action`, `context`, `context_mask`) or
|
||||
standard LeRobot keys (`observation.images.*`, `observation.state`, `action`, `action_is_pad`),
|
||||
which are adapted internally. The metrics dict includes per-term losses such as `loss_video` and
|
||||
`loss_action`.
|
||||
Args:
|
||||
batch (dict[str, Tensor]): Batch containing FastWAM-ready keys
|
||||
(`video`, `action`, `context`, `context_mask`) or LeRobot keys
|
||||
that can be adapted (`observation.images.*`, `observation.state`,
|
||||
`action`, `action_is_pad`).
|
||||
|
||||
Returns:
|
||||
tuple[Tensor, dict[str, Any]]: The scalar loss to backprop, and a dict of
|
||||
logging metrics (e.g. `loss_video`, `loss_action`) — the `(loss, output_dict)`
|
||||
contract the LeRobot training loop expects.
|
||||
"""
|
||||
|
||||
sample = self._batch_to_training_sample(batch)
|
||||
loss, metrics = self.model.training_loss(sample)
|
||||
return loss, dict(metrics or {})
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **_: Any) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
"""Predict a chunk of actions from the current FastWAM observation.
|
||||
|
||||
Accepts an inference batch with `input_image` or image-observation keys, plus a `context`/
|
||||
`context_mask` pair or a `prompt`. Returns a chunk of shape `[B, action_horizon, action_dim]`.
|
||||
Args:
|
||||
batch (dict[str, Tensor]): Inference batch with `input_image` or
|
||||
image observation keys, plus `context/context_mask` or `prompt`.
|
||||
|
||||
Returns:
|
||||
Tensor: Action chunk with shape `[B, action_horizon, action_dim]`.
|
||||
"""
|
||||
|
||||
self.eval()
|
||||
infer_kwargs = _batch_to_infer_kwargs(batch=batch, config=self.config)
|
||||
batch_size = _infer_kwargs_batch_size(infer_kwargs)
|
||||
@@ -239,7 +238,6 @@ class FastWAMPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], **kwargs: Any) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Uses an action queue populated by `predict_action_chunk`."""
|
||||
self.eval()
|
||||
if len(self._action_queue) == 0:
|
||||
actions = self.predict_action_chunk(batch, **kwargs)[:, : self.config.n_action_steps]
|
||||
|
||||
@@ -73,13 +73,14 @@ def make_fastwam_pre_post_processors(
|
||||
Args:
|
||||
config (FastWAMConfig): Policy configuration controlling device and
|
||||
normalization feature metadata.
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): Optional
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None): Optional
|
||||
LeRobot dataset statistics used by normalization processors.
|
||||
|
||||
Returns:
|
||||
tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]: Input and
|
||||
output processor pipelines discoverable by LeRobot.
|
||||
"""
|
||||
|
||||
# NOTE: no visual normalization here. VISUAL is IDENTITY (see configuration_fastwam.normalization_mapping)
|
||||
# — images pass through in [0, 1] and the model maps them to the Wan VAE's [-1, 1] at the encode
|
||||
# boundary. This is deliberate: `lerobot_train.py` overrides the normalizer stats with
|
||||
|
||||
@@ -26,7 +26,7 @@ def is_image_feature(key: str) -> bool:
|
||||
"""Check if a feature key represents an image feature.
|
||||
|
||||
Args:
|
||||
key (`str`): The feature key to check.
|
||||
key: The feature key to check
|
||||
|
||||
Returns:
|
||||
True if the key represents an image feature, False otherwise
|
||||
@@ -54,8 +54,6 @@ class ConcurrencyConfig:
|
||||
|
||||
@dataclass
|
||||
class ActorLearnerConfig:
|
||||
"""Actor-learner distributed architecture settings (network address, weight-push frequency)."""
|
||||
|
||||
learner_host: str = "127.0.0.1"
|
||||
learner_port: int = 50051
|
||||
policy_parameters_push_frequency: int = 4
|
||||
@@ -64,8 +62,6 @@ class ActorLearnerConfig:
|
||||
|
||||
@dataclass
|
||||
class CriticNetworkConfig:
|
||||
"""MLP architecture settings for the critic network(s)."""
|
||||
|
||||
hidden_dims: list[int] = field(default_factory=lambda: [256, 256])
|
||||
activate_final: bool = True
|
||||
final_activation: str | None = None
|
||||
@@ -73,16 +69,12 @@ class CriticNetworkConfig:
|
||||
|
||||
@dataclass
|
||||
class ActorNetworkConfig:
|
||||
"""MLP architecture settings for the actor network."""
|
||||
|
||||
hidden_dims: list[int] = field(default_factory=lambda: [256, 256])
|
||||
activate_final: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyConfig:
|
||||
"""Gaussian-policy output-head settings (tanh squashing, std clamping)."""
|
||||
|
||||
use_tanh_squash: bool = True
|
||||
std_min: float = 1e-5
|
||||
std_max: float = 10.0
|
||||
@@ -102,95 +94,9 @@ class GaussianActorConfig(PreTrainedConfig):
|
||||
logic live on the algorithm side (see ``lerobot.rl.algorithms.sac``).
|
||||
|
||||
CLI: ``--policy.type=gaussian_actor``.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back). This policy predicts a single action from a single step, so
|
||||
this is not expected to be changed from 1.
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str`, *optional*, defaults to `"cpu"`):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the
|
||||
policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to mean/std normalization for visual features and min/max normalization for
|
||||
state, environment, and action features.
|
||||
dataset_stats (`dict[str, dict[str, list[float]]] | None`, *optional*):
|
||||
Statistics used to normalize image, state, and action features. Defaults to placeholder
|
||||
values; normally overridden with statistics computed from the actual training dataset.
|
||||
storage_device (`str`, *optional*, defaults to `"cpu"`):
|
||||
Device on which a copy of the model's parameters is kept for transport between the actor and
|
||||
learner processes in the actor-learner architecture.
|
||||
vision_encoder_name (`str | None`, *optional*):
|
||||
Name of a pretrained vision encoder to use for image observations, e.g.
|
||||
`"lerobot/resnet10"` for the HIL-SERL ResNet10 encoder. `None` (the default) uses a
|
||||
lightweight from-scratch CNN encoder instead.
|
||||
freeze_vision_encoder (`bool`, *optional*, defaults to `True`):
|
||||
Whether to freeze the vision encoder's parameters during training.
|
||||
image_encoder_hidden_dim (`int`, *optional*, defaults to 32):
|
||||
Hidden dimension size for the from-scratch image encoder (unused when `vision_encoder_name`
|
||||
is set).
|
||||
shared_encoder (`bool`, *optional*, defaults to `True`):
|
||||
Whether the actor and critic(s) share the same observation encoder instance.
|
||||
num_discrete_actions (`int | None`, *optional*):
|
||||
Number of discrete actions appended to the continuous action output, e.g. for a gripper
|
||||
open/close action. `None` disables the discrete critic and action head.
|
||||
image_embedding_pooling_dim (`int`, *optional*, defaults to 8):
|
||||
Number of learned spatial pooling features per image, used by the image encoder's spatial
|
||||
embedding layer.
|
||||
state_encoder_hidden_dim (`int`, *optional*, defaults to 256):
|
||||
Hidden dimension size for the state encoder.
|
||||
latent_dim (`int`, *optional*, defaults to 256):
|
||||
Dimension of the observation encoder's output latent space.
|
||||
online_steps (`int`, *optional*, defaults to 1000000):
|
||||
Number of steps to run during online training.
|
||||
online_buffer_capacity (`int`, *optional*, defaults to 100000):
|
||||
Capacity of the online replay buffer.
|
||||
offline_buffer_capacity (`int`, *optional*, defaults to 100000):
|
||||
Capacity of the offline replay buffer.
|
||||
async_prefetch (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use asynchronous prefetching for the replay buffers.
|
||||
online_step_before_learning (`int`, *optional*, defaults to 100):
|
||||
Number of steps to collect before online learning starts.
|
||||
actor_learner_config (`ActorLearnerConfig`, *optional*):
|
||||
Transport configuration (host, port, push frequency, queue timeout) for the actor-learner
|
||||
architecture.
|
||||
concurrency (`ConcurrencyConfig`, *optional*):
|
||||
Concurrency configuration (threads or processes) for the actor and learner.
|
||||
actor_network_kwargs (`ActorNetworkConfig`, *optional*):
|
||||
Architecture configuration (hidden dimensions, final activation) for the actor network.
|
||||
policy_kwargs (`PolicyConfig`, *optional*):
|
||||
Configuration for the Gaussian policy head (tanh squashing, std bounds, final-layer init
|
||||
scale).
|
||||
discrete_critic_network_kwargs (`CriticNetworkConfig`, *optional*):
|
||||
Architecture configuration (hidden dimensions, final activation) for the discrete critic
|
||||
network.
|
||||
"""
|
||||
|
||||
# Mapping of feature types to normalization modes
|
||||
normalization_mapping: dict[str, NormalizationMode] = field(
|
||||
default_factory=lambda: {
|
||||
"VISUAL": NormalizationMode.MEAN_STD,
|
||||
@@ -200,6 +106,7 @@ class GaussianActorConfig(PreTrainedConfig):
|
||||
}
|
||||
)
|
||||
|
||||
# Statistics for normalizing different types of inputs
|
||||
dataset_stats: dict[str, dict[str, list[float]]] | None = field(
|
||||
default_factory=lambda: {
|
||||
OBS_IMAGE: {
|
||||
@@ -218,42 +125,60 @@ class GaussianActorConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
# Architecture specifics
|
||||
# Device to run the model on (e.g., "cuda", "cpu")
|
||||
device: str = "cpu"
|
||||
# Device to store the model on
|
||||
storage_device: str = "cpu"
|
||||
# Name of the vision encoder model (Set to "lerobot/resnet10" for hil serl resnet10)
|
||||
vision_encoder_name: str | None = None
|
||||
# Whether to freeze the vision encoder during training
|
||||
freeze_vision_encoder: bool = True
|
||||
# Hidden dimension size for the image encoder
|
||||
image_encoder_hidden_dim: int = 32
|
||||
# Whether to use a shared encoder for actor and critic
|
||||
shared_encoder: bool = True
|
||||
# Number of discrete actions, eg for gripper actions
|
||||
num_discrete_actions: int | None = None
|
||||
# Dimension of the image embedding pooling
|
||||
image_embedding_pooling_dim: int = 8
|
||||
|
||||
# Encoder architecture
|
||||
# Hidden dimension size for the state encoder
|
||||
state_encoder_hidden_dim: int = 256
|
||||
# Dimension of the latent space
|
||||
latent_dim: int = 256
|
||||
|
||||
# Online training (TODO(Khalil): relocate to TrainRLServerPipelineConfig)
|
||||
# Number of steps for online training
|
||||
online_steps: int = 1000000
|
||||
# Capacity of the online replay buffer
|
||||
online_buffer_capacity: int = 100000
|
||||
# Capacity of the offline replay buffer
|
||||
offline_buffer_capacity: int = 100000
|
||||
# Whether to use asynchronous prefetching for the buffers
|
||||
async_prefetch: bool = False
|
||||
# Number of steps before learning starts
|
||||
online_step_before_learning: int = 100
|
||||
|
||||
# Actor-learner transport (TODO(Khalil): relocate to TrainRLServerPipelineConfig).
|
||||
# Configuration for actor-learner architecture
|
||||
actor_learner_config: ActorLearnerConfig = field(default_factory=ActorLearnerConfig)
|
||||
# Configuration for concurrency settings (you can use threads or processes for the actor and learner)
|
||||
concurrency: ConcurrencyConfig = field(default_factory=ConcurrencyConfig)
|
||||
|
||||
# Network architecture
|
||||
# Configuration for the actor network architecture
|
||||
actor_network_kwargs: ActorNetworkConfig = field(default_factory=ActorNetworkConfig)
|
||||
# Configuration for the policy parameters (Gaussian head)
|
||||
policy_kwargs: PolicyConfig = field(default_factory=PolicyConfig)
|
||||
# Configuration for the discrete critic network
|
||||
discrete_critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates actor/critic network and learner configuration."""
|
||||
super().__post_init__()
|
||||
# Any validation specific to GaussianActor configuration
|
||||
|
||||
def get_optimizer_preset(self) -> MultiAdamConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
# Default learning rate used to satisfy the abstract ``get_optimizer_preset()``
|
||||
# contract from ``PreTrainedConfig``. The actual optimizers used during RL
|
||||
# training are built by ``SACAlgorithm.make_optimizers_and_scheduler()`` from
|
||||
@@ -270,11 +195,9 @@ class GaussianActorConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return None
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
has_image = any(is_image_feature(key) for key in self.input_features)
|
||||
has_state = OBS_STATE in self.input_features
|
||||
|
||||
@@ -288,20 +211,16 @@ class GaussianActorConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def image_features(self) -> list[str]:
|
||||
"""The names of the input features that are images."""
|
||||
return [key for key in self.input_features if is_image_feature(key)]
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return None # SAC typically predicts one action at a time
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -35,14 +35,6 @@ DISCRETE_DIMENSION_INDEX = -1 # Gripper is always the last dimension
|
||||
class GaussianActorPolicy(
|
||||
PreTrainedPolicy,
|
||||
):
|
||||
"""Tanh-squashed diagonal Gaussian actor policy for SAC and related maximum-entropy continuous-control
|
||||
algorithms.
|
||||
|
||||
This policy only implements the actor (and its observation encoder) plus an optional discrete-action
|
||||
critic head; the Q-critics, temperature, and Bellman-update logic live on the algorithm side (see
|
||||
`lerobot.rl.algorithms.sac`).
|
||||
"""
|
||||
|
||||
config_class = GaussianActorConfig
|
||||
name = "gaussian_actor"
|
||||
|
||||
@@ -50,11 +42,6 @@ class GaussianActorPolicy(
|
||||
self,
|
||||
config: GaussianActorConfig | None = None,
|
||||
):
|
||||
"""Build the observation encoder(s), the Gaussian actor network, and the optional discrete critic.
|
||||
|
||||
Args:
|
||||
config (GaussianActorConfig): The policy configuration.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
self.config = config
|
||||
@@ -66,12 +53,6 @@ class GaussianActorPolicy(
|
||||
self._init_discrete_critic()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Returns only the `"actor"` parameter group, excluding the shared encoder's parameters when
|
||||
`shared_encoder` is enabled. The critic, encoder, and temperature parameters are optimized
|
||||
separately by the SAC algorithm.
|
||||
"""
|
||||
optim_params = {
|
||||
"actor": [
|
||||
p
|
||||
@@ -82,30 +63,20 @@ class GaussianActorPolicy(
|
||||
return optim_params
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. This policy holds no episode-scoped state,
|
||||
so this is a no-op.
|
||||
"""
|
||||
"""Reset the policy"""
|
||||
pass
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Not supported: this policy predicts a single action per call rather than a chunk of actions, and
|
||||
calling this always raises `NotImplementedError`.
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
raise NotImplementedError(
|
||||
"GaussianActorPolicy does not support action chunking. It returns single actions!"
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
"""Select action for inference/evaluation"""
|
||||
|
||||
Samples one action directly from the actor network, re-using cached image features from the
|
||||
shared encoder when available, and appends an argmax discrete action (e.g. a gripper command)
|
||||
when `num_discrete_actions` is set.
|
||||
"""
|
||||
observations_features = None
|
||||
if self.shared_encoder and self.actor.encoder.has_images:
|
||||
observations_features = self.actor.encoder.get_cached_image_features(batch)
|
||||
@@ -125,19 +96,15 @@ class GaussianActorPolicy(
|
||||
return actions
|
||||
|
||||
def forward(self, batch: dict[str, Tensor | dict[str, Tensor]]) -> dict[str, Tensor]:
|
||||
"""Actor forward pass: sample actions and return their log-probabilities.
|
||||
|
||||
Deviates from the base contract: rather than returning a training loss, this returns the actor's
|
||||
sampled actions, log-probabilities, and means directly. Loss computation and the Bellman update
|
||||
live on the algorithm side (see `lerobot.rl.algorithms.sac`).
|
||||
"""Actor forward pass: sample actions and return log-probabilities.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor | dict[str, Tensor]]): A flat observation dict, or a training dict
|
||||
containing `"state"` (observations) and optionally `"observation_feature"`
|
||||
batch: A flat observation dict, or a training dict containing
|
||||
``"state"`` (observations) and optionally ``"observation_feature"``
|
||||
(pre-computed encoder features).
|
||||
|
||||
Returns:
|
||||
dict[str, Tensor]: Dict with `"action"`, `"log_prob"`, and `"action_mean"` tensors.
|
||||
Dict with ``"action"``, ``"log_prob"``, and ``"action_mean"`` tensors.
|
||||
"""
|
||||
observations = batch.get("state", batch)
|
||||
observation_features = batch.get("observation_feature") if isinstance(batch, dict) else None
|
||||
@@ -344,10 +311,10 @@ class MLP(nn.Module):
|
||||
Arguments:
|
||||
input_dim (int): Size of input feature dimension.
|
||||
hidden_dims (list[int]): Sizes for each hidden layer.
|
||||
activations (Callable or str, *optional*, defaults to `SiLU()`): Activation to apply between layers.
|
||||
activate_final (bool, *optional*, defaults to `False`): Whether to apply activation at the final layer.
|
||||
dropout_rate (Optional[float], *optional*): Dropout probability applied before normalization and activation.
|
||||
final_activation (Optional[Callable or str], *optional*): Activation for the final layer when `activate_final` is True.
|
||||
activations (Callable or str): Activation to apply between layers.
|
||||
activate_final (bool): Whether to apply activation at the final layer.
|
||||
dropout_rate (Optional[float]): Dropout probability applied before normalization and activation.
|
||||
final_activation (Optional[Callable or str]): Activation for the final layer when `activate_final` is True.
|
||||
|
||||
For each layer, `in_dim` is updated to the previous `out_dim`. All constructed modules are
|
||||
stored in `self.net` as an `nn.Sequential` container.
|
||||
@@ -595,7 +562,8 @@ def orthogonal_init():
|
||||
|
||||
class SpatialLearnedEmbeddings(nn.Module):
|
||||
def __init__(self, height, width, channel, num_features=8):
|
||||
"""PyTorch implementation of learned spatial embeddings
|
||||
"""
|
||||
PyTorch implementation of learned spatial embeddings
|
||||
|
||||
Args:
|
||||
height: Spatial height of input features
|
||||
@@ -614,7 +582,8 @@ class SpatialLearnedEmbeddings(nn.Module):
|
||||
nn.init.kaiming_normal_(self.kernel, mode="fan_in", nonlinearity="linear")
|
||||
|
||||
def forward(self, features):
|
||||
"""Forward pass for spatial embedding
|
||||
"""
|
||||
Forward pass for spatial embedding
|
||||
|
||||
Args:
|
||||
features: Input tensor of shape [B, C, H, W] where B is batch size,
|
||||
@@ -622,6 +591,7 @@ class SpatialLearnedEmbeddings(nn.Module):
|
||||
Returns:
|
||||
Output tensor of shape [B, C*F] where F is the number of features
|
||||
"""
|
||||
|
||||
features_expanded = features.unsqueeze(-1) # [B, C, H, W, 1]
|
||||
kernel_expanded = self.kernel.unsqueeze(0) # [1, C, H, W, F]
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ def make_gaussian_actor_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the Gaussian actor policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the Gaussian actor policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -48,8 +49,8 @@ def make_gaussian_actor_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`GaussianActorConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the tanh-Gaussian policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
|
||||
@@ -74,11 +74,6 @@ _GROOT_ACTION_DECODE_TRANSFORM_ALIASES = {
|
||||
|
||||
|
||||
def normalize_groot_model_version(model_version: str) -> str:
|
||||
"""Resolve `model_version` to a canonical GR00T version string.
|
||||
|
||||
Raises:
|
||||
ValueError: If `model_version` isn't a recognized alias.
|
||||
"""
|
||||
normalized = _GROOT_MODEL_VERSION_ALIASES.get(model_version.lower())
|
||||
if normalized is None:
|
||||
supported = GROOT_N1_7
|
||||
@@ -90,11 +85,6 @@ def normalize_groot_model_version(model_version: str) -> str:
|
||||
|
||||
|
||||
def normalize_groot_action_decode_transform(transform: str | None) -> str | None:
|
||||
"""Resolve `transform` to a canonical action-decode-transform name, or `None`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `transform` isn't a recognized alias.
|
||||
"""
|
||||
if transform is None:
|
||||
return None
|
||||
normalized = _GROOT_ACTION_DECODE_TRANSFORM_ALIASES.get(transform.lower())
|
||||
@@ -110,7 +100,6 @@ def normalize_groot_action_decode_transform(transform: str | None) -> str | None
|
||||
|
||||
|
||||
def infer_groot_model_version(model_path: str | None) -> str | None:
|
||||
"""Infer the GR00T model version (`GROOT_N1_7`) from a checkpoint path, or `None` if undetermined."""
|
||||
if not model_path:
|
||||
return None
|
||||
model_path_lower = model_path.lower()
|
||||
@@ -128,7 +117,6 @@ def infer_groot_model_version(model_path: str | None) -> str | None:
|
||||
|
||||
|
||||
def is_raw_groot_n1_7_checkpoint(model_path: str | Path | None) -> bool:
|
||||
"""Return `True` if `model_path` looks like an un-migrated, raw upstream GR00T N1.7 checkpoint."""
|
||||
if model_path is None:
|
||||
return False
|
||||
|
||||
@@ -145,7 +133,6 @@ def is_raw_groot_n1_7_checkpoint(model_path: str | Path | None) -> bool:
|
||||
|
||||
|
||||
def infer_groot_n1_7_embodiment_tag(model_path: str | Path | None) -> str | None:
|
||||
"""Infer the embodiment tag from a raw GR00T N1.7 checkpoint's `processor_config.json`, if resolvable."""
|
||||
if model_path is None:
|
||||
return None
|
||||
|
||||
@@ -165,13 +152,6 @@ def infer_groot_n1_7_embodiment_tag(model_path: str | Path | None) -> str | None
|
||||
def infer_groot_n1_7_action_horizon(
|
||||
model_path: str | Path | None, embodiment_tag: str | None = None
|
||||
) -> int | None:
|
||||
"""Infer the action horizon from a raw GR00T N1.7 checkpoint's `processor_config.json`, if resolvable.
|
||||
|
||||
Args:
|
||||
model_path (`str | pathlib.Path | None`): Path to the checkpoint directory.
|
||||
embodiment_tag (`str | None`, *optional*): The embodiment tag to look up. Inferred via
|
||||
`infer_groot_n1_7_embodiment_tag` when `None`.
|
||||
"""
|
||||
if model_path is None:
|
||||
return None
|
||||
|
||||
@@ -205,13 +185,6 @@ def infer_groot_n1_7_action_horizon(
|
||||
def infer_groot_n1_7_action_execution_horizon(
|
||||
model_path: str | Path | None, embodiment_tag: str | None = None
|
||||
) -> int | None:
|
||||
"""Infer the action execution horizon (<= action horizon) for a raw GR00T N1.7 checkpoint.
|
||||
|
||||
Args:
|
||||
model_path (`str | pathlib.Path | None`): Path to the checkpoint directory.
|
||||
embodiment_tag (`str | None`, *optional*): The embodiment tag to look up. Inferred via
|
||||
`infer_groot_n1_7_embodiment_tag` when `None`.
|
||||
"""
|
||||
action_horizon = infer_groot_n1_7_action_horizon(model_path, embodiment_tag)
|
||||
if action_horizon is None:
|
||||
return None
|
||||
@@ -268,127 +241,7 @@ def _infer_groot_model_version_from_config(config: dict) -> str | None:
|
||||
@PreTrainedConfig.register_subclass("groot")
|
||||
@dataclass
|
||||
class GrootConfig(PreTrainedConfig):
|
||||
"""Configuration for the GR00T N1.7 policy wrapper.
|
||||
|
||||
Wraps NVIDIA's Isaac-GR00T N1.7 model (a Qwen3-VL/Cosmos-Reason2 backbone plus a flow-matching
|
||||
action head) for fine-tuning and inference through LeRobot. GR00T N1.5 checkpoints and configs are
|
||||
no longer supported; loading one raises with `GROOT_N1_5_REMOVAL_GUIDANCE`.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1): Number of environment steps of observation to
|
||||
pass to the policy (the current step plus this many additional steps looking back).
|
||||
input_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from input feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from output feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*): Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. If unset or unavailable, auto-selected on construction.
|
||||
use_amp (`bool`, *optional*, defaults to `False`): Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`): Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`): Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*): Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*): Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*): Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*): License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`pathlib.Path | None`, *optional*): Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*): Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 40): The size of the action prediction chunk decoded
|
||||
per call to `predict_action_chunk`.
|
||||
n_action_steps (`int`, *optional*, defaults to 40): The number of actions from a predicted
|
||||
chunk that are actually queued for execution. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 132): Maximum observation-state dimension expected
|
||||
by the pretrained GR00T model; shorter states are zero-padded.
|
||||
max_action_dim (`int`, *optional*, defaults to 132): Maximum action dimension expected by the
|
||||
pretrained GR00T model; shorter actions are zero-padded.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*): Per-feature-type
|
||||
normalization mode. Always `IDENTITY` for every feature: GR00T normalizes state/action
|
||||
internally in its own processor steps and the Qwen3-VL image processor handles image
|
||||
normalization, so this mapping is not consulted by `make_groot_pre_post_processors`.
|
||||
base_model_path (`str | None`, *optional*): Path or Hub id of the base GR00T N1.7 model whose
|
||||
backbone weights and checkpoint sidecars (`statistics.json`, `processor_config.json`, ...)
|
||||
are loaded. Distinct from the inherited `pretrained_path`, which points at a saved LeRobot
|
||||
checkpoint directory. Defaults to `GROOT_N1_7_BASE_MODEL` when left unset.
|
||||
action_decode_transform (`str | None`, *optional*, defaults to `"auto"`): Named action transform
|
||||
applied after raw N1.7 checkpoint decoding and before `env.step()`. `"auto"` resolves to the
|
||||
embodiment default (`"libero"` for the `libero_sim` embodiment, otherwise no transform);
|
||||
pass `"none"` to explicitly disable it.
|
||||
embodiment_tag (`str`, *optional*, defaults to `"new_embodiment"`): Embodiment tag to use for
|
||||
training, e.g. `"new_embodiment"` or `"gr1"`.
|
||||
tune_llm (`bool`, *optional*, defaults to `False`): Whether to fine-tune the LLM backbone.
|
||||
tune_visual (`bool`, *optional*, defaults to `False`): Whether to fine-tune the vision tower.
|
||||
tune_projector (`bool`, *optional*, defaults to `True`): Whether to fine-tune the projector.
|
||||
tune_diffusion_model (`bool`, *optional*, defaults to `True`): Whether to fine-tune the
|
||||
flow-matching action head.
|
||||
tune_vlln (`bool`, *optional*, defaults to `True`): Whether to fine-tune the VL LayerNorm and VL
|
||||
self-attention projector in the action head.
|
||||
tune_top_llm_layers (`int`, *optional*, defaults to 0): Number of top LLM backbone layers to
|
||||
fine-tune (0 means none). Lets you adapt just the final language layers without unfreezing
|
||||
the whole backbone; independent of `tune_llm`, which tunes the entire LLM.
|
||||
num_inference_timesteps (`int | None`, *optional*): Number of flow-matching denoising steps used
|
||||
to decode an action chunk at inference time. `None` keeps the checkpoint value (GR00T N1.7
|
||||
default: 4).
|
||||
rtc_ramp_rate (`float | None`, *optional*): Real-Time Chunking overlap-blend ramp rate, used
|
||||
when the RTC engine supplies a previous-chunk prefix. `None` keeps the checkpoint value
|
||||
(GR00T N1.7 default: 6.0).
|
||||
use_flash_attention (`bool`, *optional*, defaults to `False`): Whether to request the
|
||||
flash-attention-2 kernel for the Qwen3-VL backbone. Set to `True` only after installing a
|
||||
flash-attn build matching your torch/CUDA environment; otherwise the backbone falls back to
|
||||
SDPA, which is numerically equivalent.
|
||||
use_relative_actions (`bool`, *optional*, defaults to `False`): Whether to enable GR00T-style
|
||||
state-relative action chunks (the action chunk is expressed relative to the current
|
||||
observation state).
|
||||
relative_exclude_joints (`list[str]`, *optional*): Action dimensions that stay absolute when
|
||||
`use_relative_actions` is set; matched as a case-insensitive substring against the dataset's
|
||||
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.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001): Learning rate for the AdamW optimizer.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.999)`): AdamW betas, as
|
||||
used by the Isaac-GR00T N1.7 fine-tuning recipe.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08): AdamW epsilon.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-05): AdamW weight decay.
|
||||
warmup_ratio (`float`, *optional*, defaults to 0.05): Fraction of `max_steps` used as cosine
|
||||
scheduler warmup.
|
||||
use_bf16 (`bool`, *optional*, defaults to `True`): Whether to run the GR00T forward/inference
|
||||
passes under BF16 autocast.
|
||||
model_params_fp32 (`bool`, *optional*, defaults to `True`): Whether to keep model parameters in
|
||||
FP32 while computing under BF16 autocast, matching the native N1.7 fine-tuning recipe.
|
||||
image_size (`tuple[int, int]`, *optional*, defaults to `(256, 256)`): Legacy field kept only so
|
||||
that a GR00T N1.5-era `image_size=(224, 224)` config is detected and remapped to the N1.7
|
||||
default in `__post_init__`; image sizing is otherwise handled by the backbone's image
|
||||
processor.
|
||||
tokenizer_assets_repo (`str | None`, *optional*): Deprecated GR00T N1.5 field. Must stay `None`;
|
||||
a non-`None` value is treated as an N1.5 checkpoint/config and rejected in `__post_init__`.
|
||||
lora_rank (`int`, *optional*, defaults to 0): Deprecated, never-wired LoRA field kept only so
|
||||
older saved configs still parse.
|
||||
lora_alpha (`int`, *optional*, defaults to 16): Deprecated, never-wired LoRA field kept only so
|
||||
older saved configs still parse.
|
||||
lora_dropout (`float`, *optional*, defaults to 0.1): Deprecated, never-wired LoRA field kept only
|
||||
so older saved configs still parse.
|
||||
lora_full_model (`bool`, *optional*, defaults to `False`): Deprecated, never-wired LoRA field
|
||||
kept only so older saved configs still parse.
|
||||
video_backend (`str`, *optional*, defaults to `"decord"`): Deprecated Isaac-GR00T runner field;
|
||||
unused by the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
balance_dataset_weights (`bool`, *optional*, defaults to `True`): Deprecated Isaac-GR00T runner
|
||||
field; unused by the LeRobot N1.7 implementation, kept only so older saved configs still
|
||||
parse.
|
||||
balance_trajectory_weights (`bool`, *optional*, defaults to `True`): Deprecated Isaac-GR00T
|
||||
runner field; unused by the LeRobot N1.7 implementation, kept only so older saved configs
|
||||
still parse.
|
||||
dataset_paths (`list[str] | None`, *optional*): Deprecated Isaac-GR00T runner field; unused by
|
||||
the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
output_dir (`str`, *optional*, defaults to `"./tmp/gr00t"`): Deprecated Isaac-GR00T runner field;
|
||||
unused by the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
save_steps (`int`, *optional*, defaults to 1000): Deprecated Isaac-GR00T runner field; unused by
|
||||
the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
max_steps (`int`, *optional*, defaults to 10000): Total training steps; used together with
|
||||
`warmup_ratio` to derive the cosine scheduler's warmup step count in
|
||||
`get_scheduler_preset`.
|
||||
batch_size (`int`, *optional*, defaults to 32): Deprecated Isaac-GR00T runner field; unused by
|
||||
the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
dataloader_num_workers (`int`, *optional*, defaults to 8): Deprecated Isaac-GR00T runner field;
|
||||
unused by the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
report_to (`str`, *optional*, defaults to `"wandb"`): Deprecated Isaac-GR00T runner field; unused
|
||||
by the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
resume (`bool`, *optional*, defaults to `False`): Deprecated Isaac-GR00T runner field; unused by
|
||||
the LeRobot N1.7 implementation, kept only so older saved configs still parse.
|
||||
"""
|
||||
"""Configuration for Groot policy wrapper."""
|
||||
|
||||
# Basic policy settings
|
||||
n_obs_steps: int = 1
|
||||
@@ -519,12 +372,6 @@ class GrootConfig(PreTrainedConfig):
|
||||
resume: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""Reject legacy GR00T N1.5 configs, normalize fields, and remap N1.5-era defaults.
|
||||
|
||||
Raises:
|
||||
ValueError: If `tokenizer_assets_repo` is set (an N1.5-only field), if `base_model_path`
|
||||
resolves to a GR00T N1.5 checkpoint, or if `n_action_steps` exceeds `chunk_size`.
|
||||
"""
|
||||
if self.tokenizer_assets_repo is not None:
|
||||
raise ValueError(
|
||||
"Config sets 'tokenizer_assets_repo', which only existed for GR00T N1.5; this looks "
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Groot Policy Wrapper for LeRobot Integration
|
||||
"""
|
||||
Groot Policy Wrapper for LeRobot Integration
|
||||
|
||||
Minimal integration that delegates to Isaac-GR00T N1.7 components where
|
||||
possible without porting their code. Dataset loading and training
|
||||
@@ -68,17 +69,10 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
config_class = GrootConfig
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`]. GR00T N1.7 implements RTC."""
|
||||
return True
|
||||
|
||||
def __init__(self, config: GrootConfig, **kwargs):
|
||||
"""Build the underlying GR00T N1.7 model from `config` and reset the action queue.
|
||||
|
||||
Args:
|
||||
config (GrootConfig): Policy configuration; also validated/completed via
|
||||
`config.validate_features()`.
|
||||
kwargs: Unused; accepted for interface compatibility with `PreTrainedPolicy`.
|
||||
"""
|
||||
"""Initialize Groot policy wrapper."""
|
||||
require_package("transformers", extra="groot")
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -155,7 +149,7 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
]
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue."""
|
||||
"""Reset policy state when environment resets."""
|
||||
self._action_queue = deque([], maxlen=self._action_queue_steps)
|
||||
|
||||
@classmethod
|
||||
@@ -174,40 +168,27 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
strict: bool = True,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""Load a Groot policy from either a raw N1.7 checkpoint or a fine-tuned LeRobot checkpoint.
|
||||
"""Load Groot policy from pretrained model.
|
||||
|
||||
Handles two cases:
|
||||
1. Base GR00T N1.7 models - loads the raw model
|
||||
2. Fine-tuned LeRobot checkpoints - loads config and weights from safetensors
|
||||
|
||||
Args:
|
||||
pretrained_name_or_path (str | Path): Hub id or local path to the GR00T model or the
|
||||
fine-tuned checkpoint.
|
||||
config (GrootConfig | None, *optional*): Config to use. If `None`, one is loaded from the
|
||||
checkpoint (fine-tuned case) or created with defaults (base-model case).
|
||||
force_download (bool, *optional*, defaults to `False`): Whether to force (re-)downloading
|
||||
the files, overriding the existing cache.
|
||||
resume_download (bool | None, *optional*): Deprecated; ignored by the underlying Hub client.
|
||||
proxies (dict | None, *optional*): A dictionary of proxy servers to use by protocol or
|
||||
endpoint.
|
||||
token (str | bool | None, *optional*): The token to use as HTTP bearer authorization for
|
||||
remote files.
|
||||
cache_dir (str | Path | None, *optional*): Path to the folder where cached files are stored.
|
||||
local_files_only (bool, *optional*, defaults to `False`): If `True`, avoid downloading the
|
||||
file and use the local cache only.
|
||||
revision (str | None, *optional*): Revision on the Hub: a branch name, git tag, or commit id.
|
||||
strict (bool, *optional*, defaults to `True`): Whether to require an exact match between the
|
||||
checkpoint's and the instantiated model's parameter keys.
|
||||
kwargs: For the fine-tuned-checkpoint case, forwarded to
|
||||
[`~policies.pretrained.PreTrainedPolicy.from_pretrained`]. For the base-model case,
|
||||
applied as config field overrides.
|
||||
pretrained_name_or_path: Path to the GR00T model or fine-tuned checkpoint
|
||||
config: Optional GrootConfig. If None, loads from checkpoint or creates default
|
||||
force_download: Force download even if cached
|
||||
resume_download: Resume interrupted download
|
||||
proxies: Proxy settings
|
||||
token: HuggingFace authentication token
|
||||
cache_dir: Cache directory path
|
||||
local_files_only: Only use local files
|
||||
revision: Specific model revision
|
||||
strict: Strict state dict loading
|
||||
**kwargs: Additional arguments (passed to config)
|
||||
|
||||
Returns:
|
||||
T: The loaded `GrootPolicy` instance, in eval mode.
|
||||
|
||||
Raises:
|
||||
ValueError: If `config.base_model_path` (or `pretrained_name_or_path`) resolves to an
|
||||
unsupported GR00T model version.
|
||||
Initialized GrootPolicy instance with loaded model
|
||||
"""
|
||||
requested_version = infer_groot_model_version(str(pretrained_name_or_path)) or GROOT_N1_7
|
||||
logger.info(
|
||||
@@ -304,11 +285,7 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
return policy
|
||||
|
||||
def get_optim_params(self): # type: ignore[override]
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Splits parameters into weight-decay and no-weight-decay groups, matching the Isaac-GR00T
|
||||
recipe of excluding biases and normalization parameters from weight decay.
|
||||
"""
|
||||
"""Isaac-GR00T excludes biases and normalization parameters from weight decay."""
|
||||
return self._build_weight_decay_parameter_groups(self)
|
||||
|
||||
def _resolve_action_queue_steps(self) -> int:
|
||||
@@ -330,6 +307,7 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
|
||||
def _resolve_prediction_horizon(self, actions: Tensor) -> int:
|
||||
"""Return the policy-facing action horizon for a native GR00T prediction."""
|
||||
|
||||
horizons = [actions.shape[1]]
|
||||
checkpoint_action_horizon = infer_groot_n1_7_action_horizon(
|
||||
self.config.base_model_path,
|
||||
@@ -466,10 +444,9 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
return inputs, options
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
"""Training forward pass.
|
||||
|
||||
Delegates to the underlying Isaac-GR00T model's `forward`, run under BF16 autocast when
|
||||
`config.use_bf16` is set.
|
||||
Delegates to Isaac-GR00T model.forward when inputs are compatible.
|
||||
"""
|
||||
groot_inputs = self._filter_groot_inputs(batch, include_action=True)
|
||||
|
||||
@@ -495,11 +472,12 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: object) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
"""Predict a chunk of actions for inference by delegating to Isaac-GR00T.
|
||||
|
||||
Delegates to the underlying Isaac-GR00T model's `get_action`, returning a tensor of shape
|
||||
`(B, n_action_steps, action_dim)`. LeRobot's RTC leftovers, if any, are converted into the
|
||||
native GR00T action-overlap options before calling the model.
|
||||
Returns a tensor of shape (B, n_action_steps, action_dim).
|
||||
|
||||
For N1.7, LeRobot's RTC leftovers are converted into the native GR00T
|
||||
action-overlap options before calling the underlying model.
|
||||
"""
|
||||
self.eval()
|
||||
|
||||
@@ -535,15 +513,7 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If `config.use_relative_actions` is set, since cached relative-chunk
|
||||
actions can be decoded against newer observation states; use `predict_action_chunk`
|
||||
directly instead.
|
||||
"""
|
||||
"""Select single action from action queue."""
|
||||
if getattr(self.config, "use_relative_actions", False):
|
||||
raise NotImplementedError(
|
||||
"GrootPolicy.select_action does not support relative-action policies because cached "
|
||||
|
||||
@@ -165,6 +165,7 @@ def _load_n1_7_checkpoint_processor_assets(config: GrootConfig) -> _GrootN17Chec
|
||||
Returns ``None`` for non-raw N1.7 checkpoints so the generic GR00T pipeline
|
||||
can keep using caller-provided dataset stats and config values.
|
||||
"""
|
||||
|
||||
if not is_raw_groot_n1_7_checkpoint(config.base_model_path):
|
||||
return None
|
||||
|
||||
@@ -272,6 +273,7 @@ def _load_n1_7_checkpoint_stats(
|
||||
joints. LeRobot normalizers operate over a single vector, so this function
|
||||
preserves checkpoint group order while flattening each selected statistic.
|
||||
"""
|
||||
|
||||
if raw_stats is None:
|
||||
all_stats = read_json(checkpoint_path / "statistics.json")
|
||||
raw_stats = all_stats.get(embodiment_tag)
|
||||
@@ -379,6 +381,7 @@ _GROOT_ABSENT_STANDARD_OVERRIDE_KEYS = frozenset(
|
||||
|
||||
def _drop_groot_absent_standard_overrides(overrides: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""Strip standard override keys that a GR00T pipeline has no step for."""
|
||||
|
||||
if not overrides:
|
||||
return overrides
|
||||
|
||||
@@ -411,6 +414,7 @@ def _apply_groot_step_overrides(
|
||||
silently (standard normalization keys GR00T has no step for are removed
|
||||
beforehand by ``_drop_groot_absent_standard_overrides``).
|
||||
"""
|
||||
|
||||
if not overrides:
|
||||
return
|
||||
|
||||
@@ -483,6 +487,7 @@ def make_groot_pre_post_processors_from_pretrained(
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Load Groot processors for a raw N1.7 checkpoint or a serialized LeRobot pipeline."""
|
||||
|
||||
# Drop the standard normalizer/unnormalizer override keys lerobot-train emits unconditionally:
|
||||
# GR00T has no such steps, so they would make both the raw-checkpoint and serialized override
|
||||
# paths raise. This must happen before either branch below.
|
||||
@@ -579,6 +584,7 @@ def _reconnect_groot_n1_7_pack_decode_steps(
|
||||
The pack step holds the per-instance raw-state cache that relative-action
|
||||
decoding reads its reference state from; the link itself is not serialized.
|
||||
"""
|
||||
|
||||
pack_step = next(
|
||||
(step for step in preprocessor.steps if isinstance(step, GrootN17PackInputsStep)),
|
||||
None,
|
||||
@@ -1149,13 +1155,13 @@ def make_groot_pre_post_processors(
|
||||
This mirrors SO100-style preprocessing and keeps scales consistent with GR00T.
|
||||
|
||||
Args:
|
||||
config (`GrootConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
dataset_meta (`typing.Any | None`, *optional*): Dataset metadata, forwarded to factories that need more than just `dataset_stats`.
|
||||
config: Groot configuration containing data_config, embodiment_tag, etc.
|
||||
dataset_stats: Optional per-key min/max statistics for normalization before padding.
|
||||
|
||||
Returns:
|
||||
Tuple of (preprocessor, postprocessor) pipelines
|
||||
"""
|
||||
|
||||
dataset_meta = dataset_meta or getattr(config, "_runtime_dataset_meta", None)
|
||||
checkpoint_assets = _load_n1_7_checkpoint_processor_assets(config)
|
||||
checkpoint_stats = checkpoint_assets.stats if checkpoint_assets is not None else None
|
||||
@@ -1348,6 +1354,7 @@ def _to_uint8_np_bthwc(img_t: torch.Tensor) -> np.ndarray:
|
||||
|
||||
def _align_video_horizon(video: np.ndarray, horizon: int | None) -> np.ndarray:
|
||||
"""Match the checkpoint video horizon by truncating or left-padding frames."""
|
||||
|
||||
if horizon is None or horizon <= 0:
|
||||
return video
|
||||
current = video.shape[1]
|
||||
@@ -2003,6 +2010,7 @@ class GrootN17PackInputsStep(ProcessorStep):
|
||||
|
||||
def get_cached_raw_state(self) -> dict[str, np.ndarray] | None:
|
||||
"""Return the latest unnormalized state split by checkpoint modality key."""
|
||||
|
||||
return self._last_raw_state
|
||||
|
||||
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||
@@ -2217,6 +2225,7 @@ def _n1_7_decode_stats_for_action(
|
||||
use_percentiles: bool,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Select the min/max arrays needed to decode one checkpoint action group."""
|
||||
|
||||
is_relative = use_relative_action and config_value(action_config.get("rep")) == "relative"
|
||||
modality = "relative_action" if is_relative else "action"
|
||||
stats = raw_stats.get(modality, {}).get(key, {})
|
||||
@@ -2515,7 +2524,8 @@ class GrootActionUnpackUnnormalizeStep(ProcessorStep):
|
||||
return features
|
||||
|
||||
def get_config(self) -> dict[str, Any]:
|
||||
"""Returns a serializable dictionary of the processor's configuration.
|
||||
"""
|
||||
Returns a serializable dictionary of the processor's configuration.
|
||||
|
||||
Excludes 'stats' since they are saved separately via state_dict().
|
||||
"""
|
||||
@@ -2528,7 +2538,8 @@ class GrootActionUnpackUnnormalizeStep(ProcessorStep):
|
||||
}
|
||||
|
||||
def state_dict(self) -> dict[str, torch.Tensor]:
|
||||
"""Returns normalization statistics as a flat state dictionary.
|
||||
"""
|
||||
Returns normalization statistics as a flat state dictionary.
|
||||
|
||||
This enables saving stats to safetensors files, similar to normalizer_processor.
|
||||
"""
|
||||
@@ -2543,7 +2554,8 @@ class GrootActionUnpackUnnormalizeStep(ProcessorStep):
|
||||
return flat
|
||||
|
||||
def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
|
||||
"""Loads normalization statistics from a flat state dictionary.
|
||||
"""
|
||||
Loads normalization statistics from a flat state dictionary.
|
||||
|
||||
This enables loading stats from safetensors files during from_pretrained.
|
||||
"""
|
||||
|
||||
@@ -35,103 +35,7 @@ from lerobot.utils.constants import ACTION
|
||||
@PreTrainedConfig.register_subclass("lingbot_va")
|
||||
@dataclass
|
||||
class LingBotVAConfig(PreTrainedConfig):
|
||||
"""Configuration for the native LingBot-VA policy integration in LeRobot.
|
||||
|
||||
Defaults match the upstream LIBERO configuration (`wan_va/configs/va_libero_cfg.py`) and the
|
||||
`transformer/config.json` of the released checkpoints.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1): Number of environment steps of observation to
|
||||
pass to the policy (the current step plus this many additional steps looking back).
|
||||
input_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from input feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from output feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*): Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. If unset or unavailable, auto-selected on construction.
|
||||
use_amp (`bool`, *optional*, defaults to `False`): Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`): Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`): Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*): Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*): Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*): Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*): License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`pathlib.Path | None`, *optional*): Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*): Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
patch_size (`tuple[int, int, int]`, *optional*, defaults to `(1, 2, 2)`): Wan transformer's
|
||||
spatiotemporal patch size (time, height, width).
|
||||
num_attention_heads (`int`, *optional*, defaults to 24): Number of attention heads in the Wan
|
||||
transformer.
|
||||
attention_head_dim (`int`, *optional*, defaults to 128): Dimension per attention head.
|
||||
in_channels (`int`, *optional*, defaults to 48): Number of input channels to the transformer
|
||||
(VAE latent channels).
|
||||
out_channels (`int`, *optional*, defaults to 48): Number of output channels from the
|
||||
transformer.
|
||||
action_dim (`int`, *optional*, defaults to 30): Dimension of the action stream fed to and
|
||||
predicted by the transformer.
|
||||
text_dim (`int`, *optional*, defaults to 4096): Dimension of the UMT5 text embeddings.
|
||||
freq_dim (`int`, *optional*, defaults to 256): Dimension of the sinusoidal timestep embedding.
|
||||
ffn_dim (`int`, *optional*, defaults to 14336): Hidden dimension of the transformer's
|
||||
feed-forward blocks.
|
||||
num_layers (`int`, *optional*, defaults to 30): Number of transformer layers.
|
||||
cross_attn_norm (`bool`, *optional*, defaults to `True`): Whether to normalize the
|
||||
cross-attention inputs.
|
||||
eps (`float`, *optional*, defaults to 1e-06): Epsilon used in the transformer's normalization
|
||||
layers.
|
||||
rope_max_seq_len (`int`, *optional*, defaults to 1024): Maximum sequence length for the
|
||||
transformer's rotary position embeddings.
|
||||
attn_mode (`str`, *optional*, defaults to `"torch"`): Attention backend. `"torch"` (SDPA) or
|
||||
`"flashattn"` for inference; `"flex"` for training only, and only on a recent torch.
|
||||
wan_pretrained_path (`str`, *optional*, defaults to `"robbyant/lingbot-va-base"`): Hub id or
|
||||
local directory holding the frozen VAE, UMT5 text encoder, and tokenizer sub-folders
|
||||
(diffusers layout, ~20 GB). Lazily loaded and not bundled in the checkpoint.
|
||||
dtype (`str`, *optional*, defaults to `"bfloat16"`): Transformer/VAE/text-encoder dtype:
|
||||
`"bfloat16"`, `"float16"`, or `"float32"`.
|
||||
text_encoder_device (`str`, *optional*, defaults to `"cpu"`): Device for the frozen UMT5-XXL
|
||||
text encoder, which runs once per episode. `"cpu"` frees ~11 GB of VRAM.
|
||||
obs_cam_keys (`list[str]`, *optional*): Observation camera keys, in concatenation order (order
|
||||
matters: latents are concatenated on width). Defaults to the LIBERO camera keys.
|
||||
image_hflip (`bool`, *optional*, defaults to `False`): Whether to undo the LIBERO env
|
||||
processor's extra horizontal flip, to match the model's training orientation.
|
||||
camera_layout (`str`, *optional*, defaults to `"width_concat"`): Camera latent layout:
|
||||
`"width_concat"` (cameras concatenated on width; LIBERO) or `"robotwin_tshape"` (full-res
|
||||
head plus half-res wrists in a "T"; RoboTwin).
|
||||
height (`int`, *optional*, defaults to 128): Observation image height fed to the VAE.
|
||||
width (`int`, *optional*, defaults to 128): Observation image width fed to the VAE.
|
||||
action_per_frame (`int`, *optional*, defaults to 4): Number of single-step actions decoded per
|
||||
predicted video frame.
|
||||
frame_chunk_size (`int`, *optional*, defaults to 4): Number of video frames predicted per
|
||||
autoregressive chunk.
|
||||
attn_window (`int`, *optional*, defaults to 30): Attention window size, in frames, for the
|
||||
causal streaming KV cache.
|
||||
num_inference_steps (`int`, *optional*, defaults to 20): Number of denoising steps for the
|
||||
video-latent flow-matching scheduler.
|
||||
video_exec_step (`int`, *optional*, defaults to -1): Which decoded video frame index to treat
|
||||
as "executed" for KV-cache feedback. `-1` uses the last frame.
|
||||
action_num_inference_steps (`int`, *optional*, defaults to 50): Number of denoising steps for
|
||||
the action flow-matching scheduler.
|
||||
guidance_scale (`float`, *optional*, defaults to 5.0): Classifier-free guidance scale for the
|
||||
video-latent stream.
|
||||
action_guidance_scale (`float`, *optional*, defaults to 1.0): Classifier-free guidance scale
|
||||
for the action stream.
|
||||
snr_shift (`float`, *optional*, defaults to 5.0): Flow-matching noise-schedule shift for the
|
||||
video-latent stream.
|
||||
action_snr_shift (`float`, *optional*, defaults to 0.05): Flow-matching noise-schedule shift
|
||||
for the action stream.
|
||||
max_sequence_length (`int`, *optional*, defaults to 512): Maximum UMT5 prompt length.
|
||||
used_action_channel_ids (`list[int]`, *optional*): Subset of the 30-d action space used by the
|
||||
benchmark; defaults to the first 7 channels (LIBERO's 7-DoF action). The action
|
||||
(un)normalization quantiles live in the checkpoint's `policy_postprocessor.json`, not here.
|
||||
save_predicted_video (`bool`, *optional*, defaults to `False`): Whether to VAE-decode predicted
|
||||
video latents into `self.last_predicted_frames`, opt-in for saving MP4s.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*): Per-feature-type
|
||||
normalization mode. Always `IDENTITY`: images are scaled and VAE-encoded, and actions are
|
||||
quantile-(un)normalized, inside the policy or a dedicated processor step.
|
||||
optimizer_lr (`float`, *optional*, defaults to 1e-05): AdamW learning rate.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`): AdamW betas.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08): AdamW epsilon.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.0001): AdamW weight decay.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0): Gradient clipping norm.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000): Number of linear-warmup steps
|
||||
before the constant learning-rate phase.
|
||||
"""
|
||||
"""Configuration for the native LingBot-VA policy integration in LeRobot."""
|
||||
|
||||
# Wan transformer architecture
|
||||
patch_size: tuple[int, int, int] = (1, 2, 2)
|
||||
@@ -210,11 +114,6 @@ class LingBotVAConfig(PreTrainedConfig):
|
||||
scheduler_warmup_steps: int = 1000
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate `attn_mode`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `attn_mode` is not one of `"torch"`, `"flashattn"`, or `"flex"`.
|
||||
"""
|
||||
super().__post_init__()
|
||||
if self.attn_mode not in ("torch", "flashattn", "flex"):
|
||||
raise ValueError(f"attn_mode must be one of 'torch', 'flashattn', 'flex'; got {self.attn_mode!r}")
|
||||
@@ -230,11 +129,6 @@ class LingBotVAConfig(PreTrainedConfig):
|
||||
return self.chunk_size
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""Validate and set up input/output features for LingBot-VA.
|
||||
|
||||
Raises:
|
||||
ValueError: If no visual input feature is present in `input_features`.
|
||||
"""
|
||||
image_features = [key for key, feat in self.input_features.items() if feat.type == FeatureType.VISUAL]
|
||||
if not image_features:
|
||||
raise ValueError(
|
||||
@@ -247,7 +141,6 @@ class LingBotVAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""Return the AdamW optimizer configuration built from the `optimizer_*` fields."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -257,23 +150,19 @@ class LingBotVAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> LRSchedulerConfig | None:
|
||||
"""Return the linear-warmup-then-constant scheduler configuration, matching upstream's `warmup_constant_lambda`."""
|
||||
# Upstream uses a linear warmup followed by a constant LR (warmup_constant_lambda).
|
||||
return ConstantWithWarmupSchedulerConfig(num_warmup_steps=self.scheduler_warmup_steps)
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list[int]:
|
||||
"""Return the keyframe-sampling indices used to build the observed-frame history."""
|
||||
temporal_downsample = 4
|
||||
stride = max(1, self.action_per_frame // temporal_downsample)
|
||||
return list(range(0, self.frame_chunk_size * temporal_downsample * stride, stride))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""Return indices for delta actions."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""Return indices for delta rewards (None for LingBot-VA)."""
|
||||
return None
|
||||
|
||||
@@ -66,17 +66,6 @@ class LingBotVAPolicy(PreTrainedPolicy):
|
||||
name = "lingbot_va"
|
||||
|
||||
def __init__(self, config: LingBotVAConfig, **kwargs):
|
||||
"""Build the trainable Wan dual-stream transformer and reset per-episode streaming state.
|
||||
|
||||
The VAE, UMT5 text encoder, and tokenizer are frozen and lazily loaded from
|
||||
`config.wan_pretrained_path` on first use; only the transformer is saved in the LeRobot
|
||||
checkpoint.
|
||||
|
||||
Args:
|
||||
config (LingBotVAConfig): Policy configuration; also validated/completed via
|
||||
`config.validate_features()`.
|
||||
kwargs: Unused; accepted for interface compatibility with `PreTrainedPolicy`.
|
||||
"""
|
||||
require_package("diffusers", extra="lingbot_va")
|
||||
require_package("transformers", extra="lingbot_va")
|
||||
super().__init__(config)
|
||||
@@ -157,18 +146,12 @@ class LingBotVAPolicy(PreTrainedPolicy):
|
||||
|
||||
# PreTrainedPolicy API
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Only the transformer is trainable; the VAE and text encoder stay frozen (kept outside the
|
||||
`nn.Module` registry). With PEFT/LoRA this naturally returns just the adapter params.
|
||||
"""
|
||||
# Only the transformer is trainable; the VAE / text encoder stay frozen (kept outside the
|
||||
# nn.Module registry). With PEFT/LoRA this naturally returns just the adapter params.
|
||||
return [p for p in self.transformer.parameters() if p.requires_grad]
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Resets all per-episode streaming state (KV cache, queues, frame counter).
|
||||
"""
|
||||
"""Reset all per-episode streaming state (KV cache, queues, frame counter)."""
|
||||
cfg = self.config
|
||||
self._action_queue: deque = deque(maxlen=cfg.n_action_steps)
|
||||
self._obs_buffer: list = [] # raw keyframe obs (one per env substep) observed this chunk
|
||||
@@ -340,11 +323,11 @@ class LingBotVAPolicy(PreTrainedPolicy):
|
||||
return loss, {"latent_loss": latent_loss.item(), "action_loss": action_loss.item()}
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
"""Training forward: dual-stream flow-matching loss.
|
||||
|
||||
Builds the (video-latent, action, text) training streams from a LeRobot batch
|
||||
(VAE-encoding the camera frames and UMT5-encoding the task), then runs the flow-matching
|
||||
dual-stream loss. Requires the policy to be built with `attn_mode='flex'`.
|
||||
dual-stream loss. Requires the policy to be built with ``attn_mode='flex'``.
|
||||
"""
|
||||
self._ensure_frozen_modules()
|
||||
latents, actions, actions_mask, text_emb = self._build_training_streams(batch)
|
||||
@@ -418,14 +401,12 @@ class LingBotVAPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
"""Return one action, refilling the chunk (and feeding back observed keyframes) as needed.
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`, refilling it (and feeding back
|
||||
observed keyframes) as needed. Mirrors the upstream LIBERO client loop
|
||||
(`evaluation/libero/client.py`): the first observation is the conditioning frame; every
|
||||
observation produced afterwards is buffered as a keyframe and, once the chunk's actions are
|
||||
exhausted, the buffered frames plus executed actions are fed back into the KV cache before the
|
||||
next chunk is predicted.
|
||||
Mirrors the upstream LIBERO client loop (``evaluation/libero/client.py``): the first obs is
|
||||
the conditioning frame; every observation produced afterwards is buffered as a keyframe and,
|
||||
once the chunk's actions are exhausted, the buffered frames + executed actions are fed back
|
||||
into the KV cache before the next chunk is predicted.
|
||||
"""
|
||||
self.eval()
|
||||
self._ensure_frozen_modules()
|
||||
@@ -456,11 +437,7 @@ class LingBotVAPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Runs one autoregressive chunk and returns actions of shape `[B, chunk_size, n_used]`
|
||||
(normalized).
|
||||
"""
|
||||
"""Run one autoregressive chunk and return actions ``[B, chunk_size, n_used]`` (normalized)."""
|
||||
self.eval()
|
||||
self._ensure_frozen_modules()
|
||||
self._maybe_init_prompt(batch)
|
||||
|
||||
@@ -32,149 +32,7 @@ from ..rtc.configuration_rtc import RTCConfig
|
||||
@PreTrainedConfig.register_subclass("molmoact2")
|
||||
@dataclass
|
||||
class MolmoAct2Config(PreTrainedConfig):
|
||||
"""Configuration for the MolmoAct2 policy, backed by the converted HF checkpoint implementation.
|
||||
|
||||
MolmoAct2 supports three training modes via `action_mode`: `"continuous"` (flow-matching only),
|
||||
`"discrete"` (autoregressive token prediction only), or `"both"` (joint loss). At inference,
|
||||
`inference_action_mode` selects which head generates actions.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1): Number of environment steps of observation to
|
||||
pass to the policy (the current step plus this many additional steps looking back).
|
||||
input_features (`dict[str, PolicyFeature]`, *optional*): Mapping from input feature name to its
|
||||
`PolicyFeature` (type and shape). Left empty to be inferred from the dataset.
|
||||
output_features (`dict[str, PolicyFeature]`, *optional*): Mapping from output feature name
|
||||
(e.g. `"action"`) to its `PolicyFeature`. Left empty to be inferred from the dataset.
|
||||
device (`str | None`, *optional*): Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. If unset or unavailable, auto-selected on construction.
|
||||
use_amp (`bool`, *optional*, defaults to `False`): Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`): Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`): Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*): Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*): Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*): Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*): License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`pathlib.Path | None`, *optional*): Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*): Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
checkpoint_path (`str`, *optional*, defaults to `"allenai/MolmoAct2"`): Hub id or local path of
|
||||
the pretrained MolmoAct2 HF checkpoint to load.
|
||||
checkpoint_revision (`str | None`, *optional*): Hub revision (commit hash, branch, or tag) for
|
||||
`checkpoint_path`.
|
||||
checkpoint_force_download (`bool`, *optional*, defaults to `False`): Whether to force
|
||||
re-downloading the checkpoint files, overriding the existing cache.
|
||||
chunk_size (`int`, *optional*, defaults to 30): The size of the action prediction chunk decoded
|
||||
per call to `predict_action_chunk`.
|
||||
n_action_steps (`int`, *optional*, defaults to 30): The number of actions from a predicted
|
||||
chunk that are actually queued for execution. Must not exceed `chunk_size`.
|
||||
action_mode (`str`, *optional*, defaults to `"both"`): Which action head(s) to train:
|
||||
`"continuous"`, `"discrete"`, or `"both"`.
|
||||
inference_action_mode (`str | None`, *optional*): Which action head to use at inference time,
|
||||
`"continuous"` or `"discrete"`. `None` defers to `action_mode`; must be compatible with it.
|
||||
discrete_action_tokenizer (`str`, *optional*, defaults to `"allenai/MolmoAct2-FAST-Tokenizer"`): Hub
|
||||
id of the FAST tokenizer used for discrete action generation.
|
||||
discrete_generation_max_steps (`int | None`, *optional*): Maximum number of autoregressive
|
||||
decoding steps for discrete action generation. `None` uses the checkpoint-derived default.
|
||||
norm_tag (`str | None`, *optional*): Tag identifying which normalization statistics to load
|
||||
from the checkpoint when `dataset_stats` isn't supplied to the processor factory.
|
||||
setup_type (`str`, *optional*, defaults to `""`): Setup-token identifier injected into the prompt; the empty
|
||||
default falls back to checkpoint metadata.
|
||||
control_mode (`str`, *optional*, defaults to `""`): Control-token identifier injected into the prompt; the empty
|
||||
default falls back to checkpoint metadata.
|
||||
image_keys (`list[str]`, *optional*): Explicit observation image keys to feed the model, in
|
||||
order. Falls back to checkpoint metadata, then to the visual features in `input_features`,
|
||||
when empty.
|
||||
normalize_language (`bool`, *optional*, defaults to `True`): Whether to normalize the language
|
||||
instruction text before tokenization.
|
||||
add_setup_tokens (`bool`, *optional*, defaults to `True`): Whether to inject setup tokens into
|
||||
the prompt.
|
||||
add_control_tokens (`bool`, *optional*, defaults to `True`): Whether to inject control tokens
|
||||
into the prompt.
|
||||
normalize_gripper (`bool`, *optional*, defaults to `False`): Whether to apply a dedicated
|
||||
gripper mask when normalizing/unnormalizing state and action.
|
||||
num_state_tokens (`int`, *optional*, defaults to 256): Number of tokens used to represent the
|
||||
proprioceptive state.
|
||||
max_sequence_length (`int | None`, *optional*): Maximum input sequence length. `None` uses the
|
||||
default MolmoAct2 sequence budget inferred from the fixed image/prompt/state/action token
|
||||
layout; override only for unusually long prompts.
|
||||
expected_max_action_dim (`int`, *optional*, defaults to 32): Action dimension the released
|
||||
MolmoAct2 checkpoints are fixed to; validated against the loaded checkpoint at model load.
|
||||
num_flow_timesteps (`int`, *optional*, defaults to 8): Number of flow-matching timesteps
|
||||
sampled during training.
|
||||
flow_matching_cutoff (`float`, *optional*, defaults to 1.0): Upper cutoff for the sampled
|
||||
flow-matching timestep fraction.
|
||||
flow_matching_time_offset (`float`, *optional*, defaults to 0.001): Offset applied to the
|
||||
sampled flow-matching timestep.
|
||||
flow_matching_time_scale (`float`, *optional*, defaults to 0.999): Scale applied to the sampled
|
||||
flow-matching timestep.
|
||||
flow_matching_beta_alpha (`float`, *optional*, defaults to 1.0): Alpha shape parameter of the
|
||||
Beta distribution used to sample flow-matching timesteps.
|
||||
flow_matching_beta_beta (`float`, *optional*, defaults to 1.5): Beta shape parameter of the Beta
|
||||
distribution used to sample flow-matching timesteps.
|
||||
num_inference_steps (`int | None`, *optional*): Number of flow-matching denoising steps at
|
||||
inference time. `None` keeps the checkpoint default.
|
||||
mask_action_dim_padding (`bool`, *optional*, defaults to `True`): Whether to mask out the
|
||||
zero-padded action dimensions during flow-matching denoising.
|
||||
enable_inference_cuda_graph (`bool`, *optional*, defaults to `True`): Whether to allow the
|
||||
backbone's CUDA graph manager to accelerate inference.
|
||||
per_episode_seed (`bool`, *optional*, defaults to `False`): MolmoAct2-local eval option; when
|
||||
enabled, stochastic continuous action generation uses a rollout-local generator derived
|
||||
from `eval_seed`.
|
||||
eval_seed (`int | None`, *optional*): Seed used to derive the rollout-local generator when
|
||||
`per_episode_seed` is set.
|
||||
rtc_config (`RTCConfig | None`, *optional*): Real-Time Chunking configuration. `None` disables
|
||||
RTC.
|
||||
joint_signs (`list[float] | None`, *optional*): Per-dimension sign correction applied to the
|
||||
observation state before the model and to the predicted action after it, for
|
||||
cross-calibration compatibility. Must be set together with `joint_offsets`.
|
||||
joint_offsets (`list[float] | None`, *optional*): Per-dimension offset correction applied
|
||||
alongside `joint_signs`. Must be set together with `joint_signs` and have the same length.
|
||||
enable_lora_vlm (`bool`, *optional*, defaults to `False`): Whether to apply LoRA adapters to the
|
||||
VLM instead of full fine-tuning.
|
||||
lora_rank (`int`, *optional*, defaults to 64): LoRA rank.
|
||||
lora_alpha (`int`, *optional*, defaults to 16): LoRA alpha.
|
||||
lora_dropout (`float`, *optional*, defaults to 0.05): LoRA dropout probability.
|
||||
lora_bias (`str`, *optional*, defaults to `"none"`): Which biases to train with LoRA:
|
||||
`"none"`, `"all"`, or `"lora_only"`.
|
||||
enable_lora_action_expert (`bool`, *optional*, defaults to `False`): Whether to also apply LoRA
|
||||
to the action expert. Requires `enable_lora_vlm`.
|
||||
enable_knowledge_insulation (`bool`, *optional*, defaults to `False`): Whether to stop the
|
||||
action expert's gradients from flowing back into the VLM.
|
||||
freeze_embedding (`bool`, *optional*, defaults to `True`): Whether to freeze the input
|
||||
embeddings during training.
|
||||
train_action_expert_only (`bool`, *optional*, defaults to `False`): Whether to train only the
|
||||
action expert parameters. Requires `action_mode="continuous"` and is incompatible with
|
||||
`enable_lora_vlm`.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`): Whether to enable gradient
|
||||
checkpointing on the backbone.
|
||||
model_dtype (`str`, *optional*, defaults to `"bfloat16"`): Torch dtype to load the checkpoint
|
||||
in: `"float32"`, `"bfloat16"`, or `"float16"`.
|
||||
softmax_auxiliary_loss (`bool`, *optional*, defaults to `True`): Whether to add the softmax
|
||||
z-loss auxiliary term to the discrete-token loss.
|
||||
softmax_auxiliary_loss_scale (`float`, *optional*, defaults to 0.0001): Scale of the softmax
|
||||
auxiliary z-loss term.
|
||||
discrete_loss_token_weighting (`str`, *optional*, defaults to `"root_subsegments_root_tokens"`): How
|
||||
to weight tokens in the discrete cross-entropy loss.
|
||||
optimizer_lr (`float`, *optional*, defaults to 1e-05): Base AdamW learning rate.
|
||||
optimizer_vit_lr (`float`, *optional*, defaults to 5e-06): AdamW learning rate for the vision
|
||||
tower.
|
||||
optimizer_connector_lr (`float`, *optional*, defaults to 5e-06): AdamW learning rate for the
|
||||
vision-language connector.
|
||||
optimizer_action_expert_lr (`float`, *optional*, defaults to 5e-05): AdamW learning rate for the
|
||||
action expert.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`): AdamW betas.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-06): AdamW epsilon.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.0): AdamW weight decay.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0): Gradient clipping norm.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 200): Number of warmup steps for the
|
||||
cosine-decay-with-warmup scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 100000): Number of decay steps for the
|
||||
scheduler.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 1e-06): Final learning rate at the end of
|
||||
the decay schedule.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*): Per-feature-type
|
||||
normalization mode; defaults to `IDENTITY` for vision and `QUANTILES` for state/action.
|
||||
dataset_feature_names (`dict[str, Any]`, *optional*): Per-key feature names populated by
|
||||
`set_dataset_feature_metadata`; not meant to be set directly.
|
||||
"""
|
||||
"""MolmoAct2 policy backed by the converted HF checkpoint implementation."""
|
||||
|
||||
checkpoint_path: str = "allenai/MolmoAct2"
|
||||
checkpoint_revision: str | None = None
|
||||
@@ -273,13 +131,6 @@ class MolmoAct2Config(PreTrainedConfig):
|
||||
dataset_feature_names: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate the action-mode, LoRA, and joint-frame-transform field combinations.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the cross-field constraints on `action_mode`,
|
||||
`inference_action_mode`, `joint_signs`/`joint_offsets`, `lora_*`, or the chunking/
|
||||
sequence-length fields are violated.
|
||||
"""
|
||||
super().__post_init__()
|
||||
if (self.joint_signs is None) != (self.joint_offsets is None):
|
||||
raise ValueError("joint_signs and joint_offsets must both be set or both be None.")
|
||||
@@ -348,21 +199,17 @@ class MolmoAct2Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""Return indices for delta observations (None for MolmoAct2)."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""Return indices for delta actions."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""Return indices for delta rewards (None for MolmoAct2)."""
|
||||
return None
|
||||
|
||||
def get_optimizer_preset(self) -> OptimizerConfig:
|
||||
"""Return the AdamW optimizer configuration built from the `optimizer_*` fields."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -372,7 +219,6 @@ class MolmoAct2Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> LRSchedulerConfig | None:
|
||||
"""Return the cosine-decay-with-warmup scheduler configuration built from the `scheduler_*` fields."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -381,12 +227,6 @@ class MolmoAct2Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def set_dataset_feature_metadata(self, features: dict[str, Any]) -> None:
|
||||
"""Record the dataset's action/state feature names into `dataset_feature_names`.
|
||||
|
||||
Args:
|
||||
features (dict[str, Any]): Dataset feature metadata, keyed by feature name (as found in
|
||||
`LeRobotDatasetMetadata.features`).
|
||||
"""
|
||||
self.dataset_feature_names = {}
|
||||
for key in (ACTION, OBS_STATE):
|
||||
feature = features.get(key) if isinstance(features, dict) else None
|
||||
|
||||
@@ -521,10 +521,6 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
name = "molmoact2"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`].
|
||||
|
||||
MolmoAct2 implements RTC only for the continuous (flow-matching) action head.
|
||||
"""
|
||||
return self.config.inference_action_mode == "continuous"
|
||||
|
||||
def __init__(
|
||||
@@ -535,16 +531,6 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
dataset_meta: Any | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Load the vendored HF MolmoAct2 model from `config.checkpoint_path` and reset the action queue.
|
||||
|
||||
Args:
|
||||
config (MolmoAct2Config): Policy configuration.
|
||||
inputs: Unused; accepted for interface compatibility with `PreTrainedPolicy`.
|
||||
dataset_stats (dict[str, dict[str, Tensor]] | None, *optional*): Unused by this
|
||||
constructor; normalization statistics are instead supplied to the processor factory.
|
||||
dataset_meta (Any | None, *optional*): Unused by this constructor.
|
||||
kwargs: Unused; accepted for interface compatibility with `PreTrainedPolicy`.
|
||||
"""
|
||||
super().__init__(config, *inputs, **kwargs)
|
||||
_apply_norm_tag_metadata(self.config)
|
||||
self.config.validate_features()
|
||||
@@ -624,10 +610,7 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
self.train(self.training)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Clears the action queue and the rollout-local action generator.
|
||||
"""
|
||||
"""Clear the action queue and rollout generator between episodes."""
|
||||
self._action_queue = deque(maxlen=self.config.n_action_steps)
|
||||
self._rollout_action_generator = None
|
||||
|
||||
@@ -651,7 +634,6 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
set_enabled(enabled)
|
||||
|
||||
def init_rtc_processor(self) -> None:
|
||||
"""(Re)build `self.rtc_processor` from `config.rtc_config`, or clear it when RTC is disabled."""
|
||||
self.rtc_processor = None
|
||||
if self.config.rtc_config is not None:
|
||||
self.rtc_processor = RTCProcessor(self.config.rtc_config)
|
||||
@@ -701,17 +683,6 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
raise RuntimeError("enable_lora_vlm=true, but no action_expert parameters were found.")
|
||||
|
||||
def train(self, mode: bool = True):
|
||||
"""Set training mode, keeping the backbone frozen in eval mode when `train_action_expert_only`.
|
||||
|
||||
Also toggles the inference CUDA graph managers off while training and on while evaluating.
|
||||
|
||||
Args:
|
||||
mode (bool, *optional*, defaults to `True`): Whether to set training (`True`) or
|
||||
evaluation (`False`) mode.
|
||||
|
||||
Returns:
|
||||
MolmoAct2Policy: `self`.
|
||||
"""
|
||||
super().train(mode)
|
||||
if getattr(self.config, "train_action_expert_only", False) and hasattr(self, "model"):
|
||||
self._hf_model().eval()
|
||||
@@ -748,11 +719,7 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
param.requires_grad = False
|
||||
|
||||
def get_optim_params(self) -> list[dict[str, Any]]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Splits parameters into per-component groups (vision tower, connector, action expert, and the
|
||||
rest), each with its own learning rate taken from the corresponding `optimizer_*_lr` field.
|
||||
"""
|
||||
"""Return optimizer param groups with per-component learning rates."""
|
||||
vit_params: list[Tensor] = []
|
||||
connector_params: list[Tensor] = []
|
||||
action_expert_params: list[Tensor] = []
|
||||
@@ -1611,11 +1578,7 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
batch: dict[str, Tensor],
|
||||
reduction: str = "mean",
|
||||
) -> tuple[Tensor, dict[str, Any]]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Computes the flow-matching loss, the discrete cross-entropy loss, or their sum, depending on
|
||||
`config.action_mode`.
|
||||
"""
|
||||
"""Compute training loss (flow-matching and/or discrete token loss)."""
|
||||
if reduction not in {"mean", "none"}:
|
||||
raise ValueError(f"Unsupported reduction={reduction!r}. Expected 'mean' or 'none'.")
|
||||
model_inputs = self._model_inputs(batch)
|
||||
@@ -1675,12 +1638,7 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Generates the chunk via continuous flow matching or discrete autoregressive decoding,
|
||||
depending on the resolved inference action mode; continuous generation additionally supports
|
||||
RTC when `config.rtc_config` is set.
|
||||
"""
|
||||
"""Generate an action chunk via continuous flow matching or discrete AR decoding."""
|
||||
if "action_mode" in kwargs:
|
||||
raise TypeError(
|
||||
"MolmoAct2 predict_action_chunk got unexpected keyword argument 'action_mode'; "
|
||||
@@ -1735,14 +1693,7 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`.
|
||||
|
||||
Raises:
|
||||
AssertionError: If RTC is enabled, since RTC is only supported through
|
||||
`predict_action_chunk`.
|
||||
"""
|
||||
"""Pop one action step from the queue, regenerating the chunk when empty."""
|
||||
if self._rtc_enabled():
|
||||
raise AssertionError("RTC is not supported for select_action, use it with predict_action_chunk")
|
||||
self.eval()
|
||||
|
||||
@@ -1116,27 +1116,6 @@ def make_molmoact2_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Build the pre/post-processor pipeline pair for the MolmoAct2 policy.
|
||||
|
||||
The preprocessor renames observation keys, adds a batch dimension, applies the optional
|
||||
joint-frame transform, masked-normalizes state/action with dataset statistics, and packs
|
||||
everything (video, state, action, language, setup/control tokens) into the vendored HF model's
|
||||
input format before moving tensors to `config.device`. The postprocessor reverses the
|
||||
normalization and joint-frame transform on the predicted actions.
|
||||
|
||||
Args:
|
||||
config (MolmoAct2Config): Policy configuration; supplies feature keys, checkpoint-derived
|
||||
metadata, and the normalization mapping.
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): Per-feature statistics
|
||||
used for state/action normalization. If `None` and `config.norm_tag` is set, statistics
|
||||
are instead loaded from the checkpoint's own normalization metadata.
|
||||
dataset_meta (Any | None, *optional*): Dataset metadata, used to build gripper masks for the
|
||||
masked normalizer/unnormalizer steps.
|
||||
|
||||
Returns:
|
||||
`tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]`: The `(preprocessor, postprocessor)`
|
||||
pipeline pair.
|
||||
"""
|
||||
env_action_dim = None
|
||||
if config.output_features and ACTION in config.output_features:
|
||||
env_action_dim = int(config.output_features[ACTION].shape[0])
|
||||
|
||||
@@ -28,141 +28,6 @@ class MultiTaskDiTConfig(PreTrainedConfig):
|
||||
|
||||
A transformer-based policy that supports both diffusion and flow matching objectives
|
||||
for multi-task robot learning with text and vision conditioning.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 2):
|
||||
Number of observation timesteps used for temporal context.
|
||||
input_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Input feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
output_features (`dict[str, PolicyFeature]`, *optional*):
|
||||
Output feature specification, keyed by feature name. Left empty to infer from the dataset.
|
||||
device (`str`, *optional*):
|
||||
Torch device to run the policy on, e.g. `"cuda"` or `"cpu"`. Auto-selected when unset or
|
||||
unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str`, *optional*):
|
||||
Hub repository id to push the policy to.
|
||||
private (`bool`, *optional*):
|
||||
Whether the pushed Hub repository is private.
|
||||
tags (`list[str]`, *optional*):
|
||||
Tags to attach to the policy on the Hub.
|
||||
license (`str`, *optional*):
|
||||
License identifier for the policy on the Hub.
|
||||
pretrained_path (`Path`, *optional*):
|
||||
Repo id or local directory of pretrained weights saved with `save_pretrained`. Left unset to
|
||||
initialize from scratch.
|
||||
pretrained_revision (`str`, *optional*):
|
||||
Hub revision to pin when loading `pretrained_path`.
|
||||
horizon (`int`, *optional*, defaults to 32):
|
||||
Number of action steps predicted per policy call.
|
||||
n_action_steps (`int`, *optional*, defaults to 24):
|
||||
Number of actions from a predicted chunk that are actually executed before re-querying the
|
||||
policy, roughly 0.8s of actions at 30Hz.
|
||||
objective (`str`, *optional*, defaults to `"diffusion"`):
|
||||
Action-generation objective, either `"diffusion"` or `"flow_matching"`.
|
||||
noise_scheduler_type (`str`, *optional*, defaults to `"DDPM"`):
|
||||
Diffusion noise scheduler, either `"DDPM"` or `"DDIM"`. Used when `objective="diffusion"`.
|
||||
num_train_timesteps (`int`, *optional*, defaults to 100):
|
||||
Number of diffusion timesteps used during training. Used when `objective="diffusion"`.
|
||||
beta_schedule (`str`, *optional*, defaults to `"squaredcos_cap_v2"`):
|
||||
Noise schedule type for the diffusion scheduler. Used when `objective="diffusion"`.
|
||||
beta_start (`float`, *optional*, defaults to 0.0001):
|
||||
Starting noise level of the diffusion schedule. Used when `objective="diffusion"`.
|
||||
beta_end (`float`, *optional*, defaults to 0.02):
|
||||
Ending noise level of the diffusion schedule. Used when `objective="diffusion"`.
|
||||
prediction_type (`str`, *optional*, defaults to `"epsilon"`):
|
||||
What the diffusion model predicts: `"epsilon"` for the noise, or `"sample"` for the clean
|
||||
action. Used when `objective="diffusion"`.
|
||||
clip_sample (`bool`, *optional*, defaults to `True`):
|
||||
Whether to clip samples to `clip_sample_range` during denoising. Used when
|
||||
`objective="diffusion"`.
|
||||
clip_sample_range (`float`, *optional*, defaults to 1.0):
|
||||
Clipping range `[-x, x]` applied when `clip_sample` is `True`.
|
||||
num_inference_steps (`int`, *optional*):
|
||||
Number of denoising steps at inference. Defaults to `num_train_timesteps` when left unset.
|
||||
Used when `objective="diffusion"`.
|
||||
sigma_min (`float`, *optional*, defaults to 0.0):
|
||||
Minimum noise level in the flow-matching interpolation path. Used when
|
||||
`objective="flow_matching"`.
|
||||
num_integration_steps (`int`, *optional*, defaults to 100):
|
||||
Number of ODE integration steps at inference. Used when `objective="flow_matching"`.
|
||||
integration_method (`str`, *optional*, defaults to `"euler"`):
|
||||
ODE solver for flow-matching sampling, either `"euler"` or `"rk4"`.
|
||||
timestep_sampling_strategy (`str`, *optional*, defaults to `"beta"`):
|
||||
How training timesteps are sampled for flow matching, either `"uniform"` or `"beta"`.
|
||||
timestep_sampling_s (`float`, *optional*, defaults to 0.999):
|
||||
Maximum timestep threshold, used only when `timestep_sampling_strategy="beta"`.
|
||||
timestep_sampling_alpha (`float`, *optional*, defaults to 1.5):
|
||||
Alpha parameter of the Beta distribution, used only when `timestep_sampling_strategy="beta"`.
|
||||
timestep_sampling_beta (`float`, *optional*, defaults to 1.0):
|
||||
Beta parameter of the Beta distribution, used only when `timestep_sampling_strategy="beta"`.
|
||||
hidden_dim (`int`, *optional*, defaults to 512):
|
||||
Transformer hidden dimension.
|
||||
num_layers (`int`, *optional*, defaults to 6):
|
||||
Number of transformer layers.
|
||||
num_heads (`int`, *optional*, defaults to 8):
|
||||
Number of attention heads. Must divide `hidden_dim`.
|
||||
dropout (`float`, *optional*, defaults to 0.1):
|
||||
Dropout rate applied inside the transformer.
|
||||
use_positional_encoding (`bool`, *optional*, defaults to `False`):
|
||||
Whether to add a learned absolute positional encoding to the action sequence.
|
||||
timestep_embed_dim (`int`, *optional*, defaults to 256):
|
||||
Dimensionality of the diffusion/flow-matching timestep embedding.
|
||||
use_rope (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use Rotary Position Embedding in self-attention instead of standard multi-head
|
||||
attention.
|
||||
rope_base (`float`, *optional*, defaults to 10000.0):
|
||||
Base frequency for Rotary Position Embedding. Used when `use_rope` is `True`.
|
||||
vision_encoder_name (`str`, *optional*, defaults to `"openai/clip-vit-base-patch16"`):
|
||||
Hugging Face Hub id of the CLIP vision model used to encode camera images. Must be a CLIP
|
||||
model.
|
||||
use_separate_rgb_encoder_per_camera (`bool`, *optional*, defaults to `False`):
|
||||
Whether to instantiate one vision encoder per camera view instead of sharing a single one.
|
||||
vision_encoder_lr_multiplier (`float`, *optional*, defaults to 0.1):
|
||||
Learning-rate multiplier applied to the vision encoder's parameter group.
|
||||
image_resize_shape (`tuple[int, int]`, *optional*):
|
||||
Size images are resized to before cropping. `None` skips resizing.
|
||||
image_crop_shape (`tuple[int, int]`, *optional*, defaults to `(224, 224)`):
|
||||
Crop shape applied after resizing. Disabled automatically when it does not fit within the
|
||||
(resized) image.
|
||||
image_crop_is_random (`bool`, *optional*, defaults to `True`):
|
||||
Whether to crop randomly during training. Inference always uses a center crop.
|
||||
text_encoder_name (`str`, *optional*, defaults to `"openai/clip-vit-base-patch16"`):
|
||||
Hugging Face Hub id of the CLIP text model used to encode the language instruction. Must be a
|
||||
CLIP model.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 77):
|
||||
Maximum length for tokenized text.
|
||||
tokenizer_padding (`str`, *optional*, defaults to `"max_length"`):
|
||||
Tokenizer padding strategy, either `"max_length"` or `"longest"`.
|
||||
tokenizer_padding_side (`str`, *optional*, defaults to `"right"`):
|
||||
Tokenizer padding side, either `"left"` or `"right"`.
|
||||
tokenizer_truncation (`bool`, *optional*, defaults to `True`):
|
||||
Whether to truncate sequences longer than `tokenizer_max_length`.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps each `FeatureType` to the `NormalizationMode` used to normalize/unnormalize it.
|
||||
optimizer_lr (`float`, *optional*, defaults to 2e-05):
|
||||
Learning rate used to build the default `AdamConfig` optimizer preset.
|
||||
optimizer_betas (`tuple`, *optional*, defaults to `(0.95, 0.999)`):
|
||||
Adam beta coefficients for the default optimizer preset.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Adam epsilon for the default optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.0):
|
||||
Weight decay for the default optimizer preset.
|
||||
scheduler_name (`str`, *optional*, defaults to `"cosine"`):
|
||||
Name of the learning-rate scheduler preset.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 0):
|
||||
Number of warmup steps for the learning-rate scheduler preset.
|
||||
do_mask_loss_for_padding (`bool`, *optional*, defaults to `False`):
|
||||
Whether to exclude padded action timesteps, marked by `action_is_pad`, from the loss.
|
||||
drop_n_last_frames (`int`, *optional*):
|
||||
Number of trailing frames dropped per episode when building training windows.
|
||||
Auto-computed from `horizon`, `n_action_steps`, and `n_obs_steps` in `__post_init__` when left
|
||||
unset.
|
||||
"""
|
||||
|
||||
n_obs_steps: int = 2 # Number of observation steps for temporal context
|
||||
@@ -240,7 +105,6 @@ class MultiTaskDiTConfig(PreTrainedConfig):
|
||||
drop_n_last_frames: int | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the DiT backbone and diffusion/flow-matching schedule configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
if self.drop_n_last_frames is None:
|
||||
@@ -325,7 +189,6 @@ class MultiTaskDiTConfig(PreTrainedConfig):
|
||||
raise ValueError("timestep_sampling_beta must be positive")
|
||||
|
||||
def get_optimizer_preset(self) -> AdamConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -334,7 +197,6 @@ class MultiTaskDiTConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> DiffuserSchedulerConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return DiffuserSchedulerConfig(
|
||||
name=self.scheduler_name,
|
||||
num_warmup_steps=self.scheduler_warmup_steps,
|
||||
@@ -373,25 +235,20 @@ class MultiTaskDiTConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def is_diffusion(self) -> bool:
|
||||
"""`True` if `objective` is `"diffusion"`."""
|
||||
return self.objective == "diffusion"
|
||||
|
||||
@property
|
||||
def is_flow_matching(self) -> bool:
|
||||
"""`True` if `objective` is `"flow_matching"`."""
|
||||
return self.objective == "flow_matching"
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, 1))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, 1 - self.n_obs_steps + self.horizon))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -68,21 +68,10 @@ from ..utils import populate_queues
|
||||
|
||||
|
||||
class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
"""Multi-Task Diffusion Transformer policy: a DiT that denoises action chunks conditioned on vision,
|
||||
language, and robot state, trained with either a diffusion or a flow-matching objective.
|
||||
"""
|
||||
|
||||
config_class = MultiTaskDiTConfig
|
||||
name = "multi_task_dit"
|
||||
|
||||
def __init__(self, config: MultiTaskDiTConfig, **kwargs):
|
||||
"""Build the observation encoder, the DiT noise/velocity predictor, and the training objective.
|
||||
|
||||
Args:
|
||||
config (`MultiTaskDiTConfig`):
|
||||
Policy configuration. `config.objective` selects between a `DiffusionObjective` and a
|
||||
`FlowMatchingObjective`.
|
||||
"""
|
||||
require_package("transformers", extra="multi_task_dit")
|
||||
require_package("diffusers", extra="multi_task_dit")
|
||||
super().__init__(config)
|
||||
@@ -118,11 +107,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def get_optim_params(self) -> list:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Returns two parameter groups: the vision encoder at `optimizer_lr * vision_encoder_lr_multiplier`,
|
||||
and everything else at the base `optimizer_lr`.
|
||||
"""
|
||||
"""Returns parameter groups with different learning rates for vision vs non-vision parameters"""
|
||||
non_vision_params = []
|
||||
vision_encoder_params = []
|
||||
|
||||
@@ -156,7 +141,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
return actions
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the observation and action queues used by `select_action`."""
|
||||
"""Clear observation and action queues. Should be called on `env.reset()`"""
|
||||
self._queues = {
|
||||
OBS_STATE: deque(maxlen=self.config.n_obs_steps),
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
@@ -167,11 +152,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Samples the chunk via the configured objective's `conditional_sample` (DDPM/DDIM denoising for
|
||||
`objective="diffusion"`, ODE integration for `objective="flow_matching"`).
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations"""
|
||||
self.eval()
|
||||
|
||||
for k in batch:
|
||||
@@ -191,7 +172,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Uses an action queue populated by `predict_action_chunk`."""
|
||||
"""Select a single action given environment observations"""
|
||||
if ACTION in batch:
|
||||
batch = dict(batch) # shallow copy to avoid modifying original
|
||||
batch.pop(ACTION)
|
||||
@@ -208,10 +189,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
|
||||
return action
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Computes the diffusion or flow-matching regression loss, depending on `config.objective`.
|
||||
"""
|
||||
"""Run the batch through the model and compute the loss for training"""
|
||||
batch = self._prepare_batch(batch)
|
||||
|
||||
conditioning_vec = self.observation_encoder.encode(batch)
|
||||
|
||||
@@ -36,7 +36,8 @@ def make_multi_task_dit_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for a Multi-Task DiT policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for a Multi-Task DiT policy.
|
||||
|
||||
The pre-processing pipeline prepares the input data for the model by:
|
||||
1. Renaming features.
|
||||
@@ -50,12 +51,15 @@ def make_multi_task_dit_pre_post_processors(
|
||||
2. Moving the data to the CPU.
|
||||
|
||||
Args:
|
||||
config (`MultiTaskDiTConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the Multi-Task DiT policy,
|
||||
containing feature definitions, normalization mappings, and device information.
|
||||
dataset_stats: A dictionary of statistics used for normalization.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
"""
|
||||
|
||||
steps = make_default_policy_processor_steps(config, dataset_stats, normalizer_device=config.device)
|
||||
|
||||
input_steps = [
|
||||
|
||||
@@ -28,127 +28,6 @@ DEFAULT_IMAGE_SIZE = 224
|
||||
@PreTrainedConfig.register_subclass("pi0")
|
||||
@dataclass
|
||||
class PI0Config(PreTrainedConfig):
|
||||
"""Configuration class for the PI0 flow-matching vision-language-action policy.
|
||||
|
||||
PI0 is a PyTorch port of Physical Intelligence's openpi model: a PaliGemma vision-language backbone
|
||||
paired with a smaller Gemma "action expert" that generates action chunks via flow matching.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
device (`str | None`, *optional*):
|
||||
Device to run the model on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Auto-detected when
|
||||
`None`.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether the policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Repository ID to push the trained policy to on the Hub.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub repository.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to attach to the policy's Hub repository.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Repo ID on the Hub or local directory to load pretrained weights from. The policy is
|
||||
initialized from scratch when `None`.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
|
||||
paligemma_variant (`str`, *optional*, defaults to `"gemma_2b"`):
|
||||
Which PaliGemma backbone variant to use for the vision-language encoder. Must be
|
||||
`"gemma_2b"` or `"gemma_300m"`.
|
||||
action_expert_variant (`str`, *optional*, defaults to `"gemma_300m"`):
|
||||
Which Gemma variant to use for the action expert network. Must be `"gemma_2b"` or
|
||||
`"gemma_300m"`.
|
||||
dtype (`str`, *optional*, defaults to `"float32"`):
|
||||
Model computation dtype. Must be `"bfloat16"` or `"float32"`.
|
||||
chunk_size (`int`, *optional*, defaults to 50):
|
||||
Number of action steps predicted per model invocation (called "action_horizon" in openpi).
|
||||
n_action_steps (`int`, *optional*, defaults to 50):
|
||||
Number of predicted action steps actually executed in the environment before predicting a new
|
||||
chunk. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the observation state vector is zero-padded to when shorter.
|
||||
max_action_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the action vector is zero-padded to when shorter.
|
||||
num_inference_steps (`int`, *optional*, defaults to 10):
|
||||
Number of flow-matching denoising steps performed at inference time.
|
||||
time_sampling_beta_alpha (`float`, *optional*, defaults to 1.5):
|
||||
Alpha shape parameter of the Beta distribution the flow-matching timestep is sampled from
|
||||
during training.
|
||||
time_sampling_beta_beta (`float`, *optional*, defaults to 1.0):
|
||||
Beta shape parameter of the Beta distribution the flow-matching timestep is sampled from
|
||||
during training.
|
||||
time_sampling_scale (`float`, *optional*, defaults to 0.999):
|
||||
Scale applied to the sampled Beta timestep before `time_sampling_offset` is added.
|
||||
time_sampling_offset (`float`, *optional*, defaults to 0.001):
|
||||
Offset added to the scaled Beta timestep sample.
|
||||
min_period (`float`, *optional*, defaults to 0.004):
|
||||
Minimum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
max_period (`float`, *optional*, defaults to 4.0):
|
||||
Maximum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
use_relative_actions (`bool`, *optional*, defaults to `False`):
|
||||
Whether to convert absolute actions to relative (relative to the current state) before feeding
|
||||
them to the model.
|
||||
relative_exclude_joints (`list[str]`, *optional*):
|
||||
Joint names to keep absolute (excluded from the relative conversion) when
|
||||
`use_relative_actions` is enabled. An empty list means every dimension is made relative.
|
||||
action_feature_names (`list[str] | None`, *optional*):
|
||||
Names of the action dimensions, in order. Populated at runtime from dataset metadata by
|
||||
`make_policy`.
|
||||
rtc_config (`RTCConfig | None`, *optional*):
|
||||
Real-Time Chunking configuration. `None` disables RTC inference.
|
||||
image_resolution (`tuple[int, int]`, *optional*, defaults to `(224, 224)`):
|
||||
Target `(height, width)` images are resized (with padding) to before being fed to the vision
|
||||
encoder.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of empty (zero-padded) camera views to add, for models trained with more cameras than
|
||||
are available at inference/training time.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Mapping from feature type (`"VISUAL"`, `"STATE"`, `"ACTION"`) to the `NormalizationMode` used
|
||||
for it.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to enable gradient checkpointing to reduce memory usage during training.
|
||||
compile_model (`bool`, *optional*, defaults to `False`):
|
||||
Whether to compile the model with `torch.compile`.
|
||||
compile_mode (`str`, *optional*, defaults to `"max-autotune"`):
|
||||
The `torch.compile` mode to use when `compile_model` is enabled.
|
||||
freeze_vision_encoder (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the vision encoder's weights during training.
|
||||
train_expert_only (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the entire VLM and train only the action expert and its projections.
|
||||
optimizer_lr (`float`, *optional*, defaults to 2.5e-05):
|
||||
Peak learning rate for the AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`):
|
||||
AdamW `(beta1, beta2)` coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
AdamW epsilon term for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.01):
|
||||
AdamW weight decay coefficient.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0):
|
||||
Maximum gradient norm for clipping.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the learning rate scheduler. Auto-scales down when the total number
|
||||
of training steps is smaller.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Learning rate the scheduler decays to at the end of `scheduler_decay_steps`.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 48):
|
||||
Maximum token length for the language tokenizer.
|
||||
"""
|
||||
|
||||
paligemma_variant: str = "gemma_2b"
|
||||
action_expert_variant: str = "gemma_300m"
|
||||
dtype: str = "float32" # Options: "bfloat16", "float32"
|
||||
@@ -224,7 +103,6 @@ class PI0Config(PreTrainedConfig):
|
||||
tokenizer_max_length: int = 48 # see openpi `__post_init__`
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the PaliGemma backbone configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
# Validate configuration
|
||||
@@ -267,7 +145,6 @@ class PI0Config(PreTrainedConfig):
|
||||
self.output_features[ACTION] = action_feature
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -277,7 +154,6 @@ class PI0Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -287,15 +163,12 @@ class PI0Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -744,17 +744,12 @@ class PI0Pytorch(nn.Module): # see openpi `PI0Pytorch`
|
||||
|
||||
|
||||
class PI0Policy(PreTrainedPolicy):
|
||||
"""PyTorch port of Physical Intelligence's PI0 vision-language-action policy, generating action
|
||||
chunks via flow matching.
|
||||
"""
|
||||
"""PI0 OpenPI Policy for LeRobot."""
|
||||
|
||||
config_class = PI0Config
|
||||
name = "pi0"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`]. PI0 implements Real-Time Chunking
|
||||
inference.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
@@ -762,10 +757,9 @@ class PI0Policy(PreTrainedPolicy):
|
||||
config: PI0Config,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the underlying PI0 model from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`PI0Config`): Policy configuration class instance.
|
||||
config: Policy configuration class instance.
|
||||
"""
|
||||
require_package("transformers", extra="pi")
|
||||
super().__init__(config)
|
||||
@@ -800,11 +794,7 @@ class PI0Policy(PreTrainedPolicy):
|
||||
strict: bool = True,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`].
|
||||
|
||||
Additionally remaps checkpoint state-dict keys from the upstream openpi naming convention before
|
||||
loading them, and defaults `strict` to `True` rather than `False`.
|
||||
"""
|
||||
"""Override the from_pretrained method to handle key remapping and display important disclaimer."""
|
||||
print(
|
||||
"The PI0 model is a direct port of the OpenPI implementation. \n"
|
||||
"This implementation follows the original OpenPI structure for compatibility. \n"
|
||||
@@ -965,13 +955,10 @@ class PI0Policy(PreTrainedPolicy):
|
||||
return fixed_state_dict
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.parameters()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
"""Reset internal state - called when environment resets."""
|
||||
self._action_queue = deque(maxlen=self.config.n_action_steps)
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
@@ -1059,20 +1046,18 @@ class PI0Policy(PreTrainedPolicy):
|
||||
return images, img_masks
|
||||
|
||||
def prepare_state(self, batch):
|
||||
"""Zero-pad the observation state to `config.max_state_dim`."""
|
||||
"""Pad state"""
|
||||
state = pad_vector(batch[OBS_STATE], self.config.max_state_dim)
|
||||
return state
|
||||
|
||||
def prepare_action(self, batch):
|
||||
"""Zero-pad the target action to `config.max_action_dim`."""
|
||||
"""Pad action"""
|
||||
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
|
||||
return actions
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Pops one action off an internal
|
||||
queue, refilling the queue by calling `predict_action_chunk` whenever it is empty.
|
||||
"""
|
||||
"""Select a single action given environment observations."""
|
||||
assert not self._rtc_enabled(), (
|
||||
"RTC is not supported for select_action, use it with predict_action_chunk"
|
||||
)
|
||||
@@ -1089,9 +1074,7 @@ class PI0Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Runs the flow-matching
|
||||
sampler (`config.num_inference_steps` denoising steps) to generate the chunk.
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
self.eval()
|
||||
|
||||
# Prepare inputs
|
||||
@@ -1109,19 +1092,13 @@ class PI0Policy(PreTrainedPolicy):
|
||||
return actions
|
||||
|
||||
def forward(self, batch: dict[str, Tensor], reduction: str = "mean") -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the flow-matching loss between
|
||||
the model's predicted and target velocity fields.
|
||||
"""Run the batch through the model and compute the loss for training.
|
||||
|
||||
Args:
|
||||
batch (`dict[str, Tensor]`):
|
||||
A batch of preprocessed, normalized observation/action tensors, as produced by this
|
||||
policy's preprocessor pipeline.
|
||||
reduction (`str`, *optional*, defaults to `"mean"`):
|
||||
How to reduce the per-element loss. `"mean"` returns a scalar mean loss; `"none"` returns
|
||||
per-sample losses of shape `(batch_size,)`, e.g. for RA-BC weighting.
|
||||
|
||||
Returns:
|
||||
`tuple[Tensor, dict]`: The loss and a dict of logging-friendly loss statistics.
|
||||
batch: Training batch containing observations and actions.
|
||||
reduction: How to reduce the loss. Options:
|
||||
- "mean": Return scalar mean loss (default, backward compatible)
|
||||
- "none": Return per-sample losses of shape (batch_size,) for RA-BC weighting
|
||||
"""
|
||||
# Prepare inputs
|
||||
images, img_masks = self._preprocess_images(batch)
|
||||
|
||||
@@ -37,7 +37,8 @@ from .configuration_pi0 import PI0Config
|
||||
|
||||
@ProcessorStepRegistry.register(name="pi0_new_line_processor")
|
||||
class Pi0NewLineProcessor(ComplementaryDataProcessorStep):
|
||||
"""Ensures that the task description string ends with a newline character.
|
||||
"""
|
||||
Ensures that the task description string ends with a newline character.
|
||||
|
||||
This processing step is required for compatibility with the PaliGemma tokenizer,
|
||||
which expects a newline at the end of the text prompt. It handles both single
|
||||
@@ -45,7 +46,8 @@ class Pi0NewLineProcessor(ComplementaryDataProcessorStep):
|
||||
"""
|
||||
|
||||
def complementary_data(self, complementary_data):
|
||||
"""Adds a newline to the 'task' field if it doesn't already have one.
|
||||
"""
|
||||
Adds a newline to the 'task' field if it doesn't already have one.
|
||||
|
||||
Args:
|
||||
complementary_data: A dictionary that may contain a 'task' key with a
|
||||
@@ -78,7 +80,8 @@ class Pi0NewLineProcessor(ComplementaryDataProcessorStep):
|
||||
def transform_features(
|
||||
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
|
||||
) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
|
||||
"""This step does not alter the feature definitions.
|
||||
"""
|
||||
This step does not alter the feature definitions.
|
||||
|
||||
Args:
|
||||
features: The input feature dictionary.
|
||||
@@ -96,7 +99,8 @@ def make_pi0_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the PI0 policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the PI0 policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -111,12 +115,15 @@ def make_pi0_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`PI0Config`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the PI0 policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
preprocessor_kwargs: Additional arguments for the pre-processor pipeline.
|
||||
postprocessor_kwargs: Additional arguments for the post-processor pipeline.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
"""
|
||||
|
||||
relative_step = RelativeActionsProcessorStep(
|
||||
enabled=config.use_relative_actions,
|
||||
exclude_joints=getattr(config, "relative_exclude_joints", []),
|
||||
|
||||
@@ -28,129 +28,6 @@ DEFAULT_IMAGE_SIZE = 224
|
||||
@PreTrainedConfig.register_subclass("pi05")
|
||||
@dataclass
|
||||
class PI05Config(PreTrainedConfig):
|
||||
"""Configuration class for the PI0.5 flow-matching vision-language-action policy.
|
||||
|
||||
PI0.5 is a PyTorch port of Physical Intelligence's openpi model: a PaliGemma vision-language backbone
|
||||
paired with a smaller Gemma "action expert" that generates action chunks via flow matching. Unlike
|
||||
PI0, it conditions the action expert on the VLM's outputs directly rather than on a separate
|
||||
proprioceptive state projection, and defaults to quantile normalization.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
device (`str | None`, *optional*):
|
||||
Device to run the model on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Auto-detected when
|
||||
`None`.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether the policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Repository ID to push the trained policy to on the Hub.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub repository.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to attach to the policy's Hub repository.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Repo ID on the Hub or local directory to load pretrained weights from. The policy is
|
||||
initialized from scratch when `None`.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
|
||||
paligemma_variant (`str`, *optional*, defaults to `"gemma_2b"`):
|
||||
Which PaliGemma backbone variant to use for the vision-language encoder. Must be
|
||||
`"gemma_2b"` or `"gemma_300m"`.
|
||||
action_expert_variant (`str`, *optional*, defaults to `"gemma_300m"`):
|
||||
Which Gemma variant to use for the action expert network. Must be `"gemma_2b"` or
|
||||
`"gemma_300m"`.
|
||||
dtype (`str`, *optional*, defaults to `"float32"`):
|
||||
Model computation dtype. Must be `"bfloat16"` or `"float32"`.
|
||||
chunk_size (`int`, *optional*, defaults to 50):
|
||||
Number of action steps predicted per model invocation (called "action_horizon" in openpi).
|
||||
n_action_steps (`int`, *optional*, defaults to 50):
|
||||
Number of predicted action steps actually executed in the environment before predicting a new
|
||||
chunk. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the observation state vector is zero-padded to when shorter.
|
||||
max_action_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the action vector is zero-padded to when shorter.
|
||||
num_inference_steps (`int`, *optional*, defaults to 10):
|
||||
Number of flow-matching denoising steps performed at inference time.
|
||||
time_sampling_beta_alpha (`float`, *optional*, defaults to 1.5):
|
||||
Alpha shape parameter of the Beta distribution the flow-matching timestep is sampled from
|
||||
during training.
|
||||
time_sampling_beta_beta (`float`, *optional*, defaults to 1.0):
|
||||
Beta shape parameter of the Beta distribution the flow-matching timestep is sampled from
|
||||
during training.
|
||||
time_sampling_scale (`float`, *optional*, defaults to 0.999):
|
||||
Scale applied to the sampled Beta timestep before `time_sampling_offset` is added.
|
||||
time_sampling_offset (`float`, *optional*, defaults to 0.001):
|
||||
Offset added to the scaled Beta timestep sample.
|
||||
min_period (`float`, *optional*, defaults to 0.004):
|
||||
Minimum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
max_period (`float`, *optional*, defaults to 4.0):
|
||||
Maximum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
use_relative_actions (`bool`, *optional*, defaults to `False`):
|
||||
Whether to convert absolute actions to relative (relative to the current state) before feeding
|
||||
them to the model.
|
||||
relative_exclude_joints (`list[str]`, *optional*):
|
||||
Joint names to keep absolute (excluded from the relative conversion) when
|
||||
`use_relative_actions` is enabled. An empty list means every dimension is made relative.
|
||||
action_feature_names (`list[str] | None`, *optional*):
|
||||
Names of the action dimensions, in order. Populated at runtime from dataset metadata by
|
||||
`make_policy`.
|
||||
rtc_config (`RTCConfig | None`, *optional*):
|
||||
Real-Time Chunking configuration. `None` disables RTC inference.
|
||||
image_resolution (`tuple[int, int]`, *optional*, defaults to `(224, 224)`):
|
||||
Target `(height, width)` images are resized (with padding) to before being fed to the vision
|
||||
encoder.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of empty (zero-padded) camera views to add, for models trained with more cameras than
|
||||
are available at inference/training time.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 200):
|
||||
Maximum token length for the language tokenizer.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Mapping from feature type (`"VISUAL"`, `"STATE"`, `"ACTION"`) to the `NormalizationMode` used
|
||||
for it. Defaults to quantile normalization for state and action, as used by PI0.5.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to enable gradient checkpointing to reduce memory usage during training.
|
||||
compile_model (`bool`, *optional*, defaults to `False`):
|
||||
Whether to compile the model with `torch.compile`.
|
||||
compile_mode (`str`, *optional*, defaults to `"max-autotune"`):
|
||||
The `torch.compile` mode to use when `compile_model` is enabled.
|
||||
freeze_vision_encoder (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the vision encoder's weights during training.
|
||||
train_expert_only (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the entire VLM and train only the action expert and its projections.
|
||||
optimizer_lr (`float`, *optional*, defaults to 2.5e-05):
|
||||
Peak learning rate for the AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`):
|
||||
AdamW `(beta1, beta2)` coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
AdamW epsilon term for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.01):
|
||||
AdamW weight decay coefficient.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0):
|
||||
Maximum gradient norm for clipping.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the learning rate scheduler. Auto-scales down when the total number
|
||||
of training steps is smaller.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Learning rate the scheduler decays to at the end of `scheduler_decay_steps`.
|
||||
"""
|
||||
|
||||
paligemma_variant: str = "gemma_2b"
|
||||
action_expert_variant: str = "gemma_300m"
|
||||
dtype: str = "float32" # Options: "bfloat16", "float32"
|
||||
@@ -227,7 +104,6 @@ class PI05Config(PreTrainedConfig):
|
||||
tokenizer_max_length: int = 200 # see openpi `__post_init__`
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the PaliGemma backbone configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
# Validate configuration
|
||||
@@ -270,7 +146,6 @@ class PI05Config(PreTrainedConfig):
|
||||
self.output_features[ACTION] = action_feature
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -280,7 +155,6 @@ class PI05Config(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -290,15 +164,12 @@ class PI05Config(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -709,17 +709,12 @@ class PI05Pytorch(nn.Module): # see openpi `PI0Pytorch`
|
||||
|
||||
|
||||
class PI05Policy(PreTrainedPolicy):
|
||||
"""PyTorch port of Physical Intelligence's PI0.5 vision-language-action policy, generating action
|
||||
chunks via flow matching.
|
||||
"""
|
||||
"""PI05 Policy for LeRobot."""
|
||||
|
||||
config_class = PI05Config
|
||||
name = "pi05"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`]. PI0.5 implements Real-Time Chunking
|
||||
inference.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
@@ -727,10 +722,9 @@ class PI05Policy(PreTrainedPolicy):
|
||||
config: PI05Config,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the underlying PI0.5 model from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`PI05Config`): Policy configuration class instance.
|
||||
config: Policy configuration class instance.
|
||||
"""
|
||||
require_package("transformers", extra="pi")
|
||||
super().__init__(config)
|
||||
@@ -765,11 +759,7 @@ class PI05Policy(PreTrainedPolicy):
|
||||
strict: bool = True,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`].
|
||||
|
||||
Additionally remaps checkpoint state-dict keys from the upstream openpi naming convention before
|
||||
loading them, and defaults `strict` to `True` rather than `False`.
|
||||
"""
|
||||
"""Override the from_pretrained method to handle key remapping and display important disclaimer."""
|
||||
print(
|
||||
"The PI05 model is a direct port of the OpenPI implementation. \n"
|
||||
"This implementation follows the original OpenPI structure for compatibility. \n"
|
||||
@@ -934,13 +924,10 @@ class PI05Policy(PreTrainedPolicy):
|
||||
return fixed_state_dict
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.parameters()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
"""Reset internal state - called when environment resets."""
|
||||
self._action_queue = deque(maxlen=self.config.n_action_steps)
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
@@ -1029,15 +1016,13 @@ class PI05Policy(PreTrainedPolicy):
|
||||
return images, img_masks
|
||||
|
||||
def prepare_action(self, batch):
|
||||
"""Zero-pad the target action to `config.max_action_dim`."""
|
||||
"""Pad action"""
|
||||
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
|
||||
return actions
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Pops one action off an internal
|
||||
queue, refilling the queue by calling `predict_action_chunk` whenever it is empty.
|
||||
"""
|
||||
"""Select a single action given environment observations."""
|
||||
assert not self._rtc_enabled(), (
|
||||
"RTC is not supported for select_action, use it with predict_action_chunk"
|
||||
)
|
||||
@@ -1054,9 +1039,7 @@ class PI05Policy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Runs the flow-matching
|
||||
sampler (`config.num_inference_steps` denoising steps) to generate the chunk.
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
self.eval()
|
||||
|
||||
# Prepare inputs
|
||||
@@ -1073,19 +1056,13 @@ class PI05Policy(PreTrainedPolicy):
|
||||
return actions
|
||||
|
||||
def forward(self, batch: dict[str, Tensor], reduction: str = "mean") -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the flow-matching loss between
|
||||
the model's predicted and target velocity fields.
|
||||
"""Run the batch through the model and compute the loss for training.
|
||||
|
||||
Args:
|
||||
batch (`dict[str, Tensor]`):
|
||||
A batch of preprocessed, normalized observation/action tensors, as produced by this
|
||||
policy's preprocessor pipeline.
|
||||
reduction (`str`, *optional*, defaults to `"mean"`):
|
||||
How to reduce the per-element loss. `"mean"` returns a scalar mean loss; `"none"` returns
|
||||
per-sample losses of shape `(batch_size,)`, e.g. for RA-BC weighting.
|
||||
|
||||
Returns:
|
||||
`tuple[Tensor, dict]`: The loss and a dict of logging-friendly loss statistics.
|
||||
batch: Training batch containing observations and actions.
|
||||
reduction: How to reduce the loss. Options:
|
||||
- "mean": Return scalar mean loss (default, backward compatible)
|
||||
- "none": Return per-sample losses of shape (batch_size,) for RA-BC weighting
|
||||
"""
|
||||
# Prepare inputs
|
||||
images, img_masks = self._preprocess_images(batch)
|
||||
|
||||
@@ -42,7 +42,9 @@ from .configuration_pi05 import PI05Config
|
||||
@ProcessorStepRegistry.register(name="pi05_prepare_state_tokenizer_processor_step")
|
||||
@dataclass
|
||||
class Pi05PrepareStateTokenizerProcessorStep(ProcessorStep):
|
||||
"""Processor step to prepare the state and tokenize the language input."""
|
||||
"""
|
||||
Processor step to prepare the state and tokenize the language input.
|
||||
"""
|
||||
|
||||
max_state_dim: int = 32
|
||||
task_key: str = "task"
|
||||
@@ -80,7 +82,9 @@ class Pi05PrepareStateTokenizerProcessorStep(ProcessorStep):
|
||||
def transform_features(
|
||||
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
|
||||
) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
|
||||
"""This step does not alter the feature definitions."""
|
||||
"""
|
||||
This step does not alter the feature definitions.
|
||||
"""
|
||||
return features
|
||||
|
||||
|
||||
@@ -91,7 +95,8 @@ def make_pi05_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the PI0 policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the PI0 policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -106,12 +111,15 @@ def make_pi05_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`PI05Config`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the PI0 policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
preprocessor_kwargs: Additional arguments for the pre-processor pipeline.
|
||||
postprocessor_kwargs: Additional arguments for the post-processor pipeline.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
"""
|
||||
|
||||
relative_step = RelativeActionsProcessorStep(
|
||||
enabled=config.use_relative_actions,
|
||||
exclude_joints=getattr(config, "relative_exclude_joints", []),
|
||||
|
||||
@@ -28,126 +28,6 @@ DEFAULT_IMAGE_SIZE = 224
|
||||
@PreTrainedConfig.register_subclass("pi0_fast")
|
||||
@dataclass
|
||||
class PI0FastConfig(PreTrainedConfig):
|
||||
"""Configuration class for the PI0-FAST autoregressive vision-language-action policy.
|
||||
|
||||
PI0-FAST is a PyTorch port of Physical Intelligence's openpi FAST model: a PaliGemma vision-language
|
||||
backbone paired with a Gemma action expert that generates actions autoregressively as discrete FAST
|
||||
tokens, rather than via flow matching.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
device (`str | None`, *optional*):
|
||||
Device to run the model on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Auto-detected when
|
||||
`None`.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether the policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Repository ID to push the trained policy to on the Hub.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub repository.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to attach to the policy's Hub repository.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Repo ID on the Hub or local directory to load pretrained weights from. The policy is
|
||||
initialized from scratch when `None`.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
|
||||
paligemma_variant (`str`, *optional*, defaults to `"gemma_2b"`):
|
||||
Which PaliGemma backbone variant to use for the vision-language encoder. Must be
|
||||
`"gemma_2b"` or `"gemma_300m"`.
|
||||
action_expert_variant (`str`, *optional*, defaults to `"gemma_300m"`):
|
||||
Which Gemma variant to use for the action expert network.
|
||||
dtype (`str`, *optional*, defaults to `"float32"`):
|
||||
Model computation dtype. Must be `"bfloat16"` or `"float32"`.
|
||||
chunk_size (`int`, *optional*, defaults to 50):
|
||||
Number of action steps predicted per model invocation (called "action_horizon" in openpi).
|
||||
n_action_steps (`int`, *optional*, defaults to 50):
|
||||
Number of predicted action steps actually executed in the environment before predicting a new
|
||||
chunk. Must not exceed `chunk_size`.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the observation state vector is zero-padded to when shorter.
|
||||
max_action_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the action vector is zero-padded to when shorter.
|
||||
max_action_tokens (`int`, *optional*, defaults to 256):
|
||||
Maximum number of discrete FAST action tokens generated per action chunk.
|
||||
use_relative_actions (`bool`, *optional*, defaults to `False`):
|
||||
Whether to convert absolute actions to relative (relative to the current state) before feeding
|
||||
them to the model.
|
||||
relative_exclude_joints (`list[str]`, *optional*):
|
||||
Joint names to keep absolute (excluded from the relative conversion) when
|
||||
`use_relative_actions` is enabled. An empty list means every dimension is made relative.
|
||||
action_feature_names (`list[str] | None`, *optional*):
|
||||
Names of the action dimensions, in order. Populated at runtime from dataset metadata by
|
||||
`make_policy`.
|
||||
rtc_config (`RTCConfig | None`, *optional*):
|
||||
Real-Time Chunking configuration. `None` disables RTC inference.
|
||||
image_resolution (`tuple[int, int]`, *optional*, defaults to `(224, 224)`):
|
||||
Target `(height, width)` images are resized (with padding) to before being fed to the vision
|
||||
encoder.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of empty (zero-padded) camera views to add, for models trained with more cameras than
|
||||
are available at inference/training time.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 200):
|
||||
Maximum token length for the language tokenizer.
|
||||
text_tokenizer_name (`str`, *optional*, defaults to `"google/paligemma-3b-pt-224"`):
|
||||
Hub identifier of the PaliGemma text tokenizer used for the language prompt.
|
||||
action_tokenizer_name (`str`, *optional*, defaults to `"lerobot/fast-action-tokenizer"`):
|
||||
Hub identifier of the FAST tokenizer used to discretize and decode actions.
|
||||
temperature (`float`, *optional*, defaults to 0.0):
|
||||
Sampling temperature used when autoregressively decoding action tokens. `0.0` means greedy
|
||||
decoding.
|
||||
max_decoding_steps (`int`, *optional*, defaults to 256):
|
||||
Maximum number of autoregressive decoding steps when generating action tokens.
|
||||
fast_skip_tokens (`int`, *optional*, defaults to 128):
|
||||
Number of vocabulary tokens reserved (skipped) between the PaliGemma text vocabulary and the
|
||||
FAST action-token range.
|
||||
validate_action_token_prefix (`bool`, *optional*, defaults to `True`):
|
||||
Whether to assert that decoded action-token sequences start with the expected `"Action: "`
|
||||
prefix.
|
||||
use_kv_cache (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use a key/value cache for faster autoregressive decoding.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Mapping from feature type (`"VISUAL"`, `"STATE"`, `"ACTION"`) to the `NormalizationMode` used
|
||||
for it.
|
||||
gradient_checkpointing (`bool`, *optional*, defaults to `False`):
|
||||
Whether to enable gradient checkpointing to reduce memory usage during training.
|
||||
compile_model (`bool`, *optional*, defaults to `False`):
|
||||
Whether to compile the model with `torch.compile`.
|
||||
compile_mode (`str`, *optional*, defaults to `"max-autotune"`):
|
||||
The `torch.compile` mode to use when `compile_model` is enabled.
|
||||
optimizer_lr (`float`, *optional*, defaults to 2.5e-05):
|
||||
Peak learning rate for the AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`):
|
||||
AdamW `(beta1, beta2)` coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
AdamW epsilon term for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.01):
|
||||
AdamW weight decay coefficient.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0):
|
||||
Maximum gradient norm for clipping.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the learning rate scheduler. Auto-scales down when the total number
|
||||
of training steps is smaller.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Learning rate the scheduler decays to at the end of `scheduler_decay_steps`.
|
||||
"""
|
||||
|
||||
paligemma_variant: str = "gemma_2b"
|
||||
action_expert_variant: str = "gemma_300m"
|
||||
dtype: str = "float32" # Options: "bfloat16", "float32"
|
||||
@@ -220,7 +100,6 @@ class PI0FastConfig(PreTrainedConfig):
|
||||
scheduler_decay_lr: float = 2.5e-6
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the PaliGemma/FAST-tokenizer configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
# Validate configuration
|
||||
@@ -260,7 +139,6 @@ class PI0FastConfig(PreTrainedConfig):
|
||||
self.output_features[ACTION] = action_feature
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -270,7 +148,6 @@ class PI0FastConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -280,15 +157,12 @@ class PI0FastConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -507,7 +507,8 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
|
||||
max_decoding_steps=None,
|
||||
temperature=0.0,
|
||||
) -> torch.Tensor:
|
||||
"""Inefficient but safe autoregressive decoding for FAST tokens.
|
||||
"""
|
||||
Inefficient but safe autoregressive decoding for FAST tokens.
|
||||
Matches the pattern of _generate_subtask_tokens.
|
||||
TODO: jadechoghari, should we move this logic to PI0FastPolicy class?
|
||||
"""
|
||||
@@ -601,7 +602,8 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
|
||||
max_decoding_steps=None,
|
||||
temperature=0.0,
|
||||
) -> torch.Tensor:
|
||||
"""Optimized autoregressive decoding for FAST tokens using KV Caching.
|
||||
"""
|
||||
Optimized autoregressive decoding for FAST tokens using KV Caching.
|
||||
|
||||
Greedy decoding stops once every sequence emits the end-of-action marker. The
|
||||
returned tensor keeps its fixed shape, with positions not generated after the
|
||||
@@ -736,9 +738,7 @@ class PI0FastPytorch(nn.Module): # see openpi `PI0Pytorch`
|
||||
|
||||
|
||||
class PI0FastPolicy(PreTrainedPolicy):
|
||||
"""PyTorch port of Physical Intelligence's PI0-FAST vision-language-action policy, generating actions
|
||||
autoregressively as discrete FAST tokens.
|
||||
"""
|
||||
"""PI0Fast Policy for LeRobot."""
|
||||
|
||||
config_class = PI0FastConfig
|
||||
name = "pi0_fast"
|
||||
@@ -748,10 +748,9 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
config: PI0FastConfig,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the underlying PI0-FAST model and its tokenizers from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`PI0FastConfig`): Policy configuration class instance.
|
||||
config: Policy configuration class instance.
|
||||
"""
|
||||
require_package("transformers", extra="pi")
|
||||
require_package("scipy", extra="pi")
|
||||
@@ -807,11 +806,7 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
strict: bool = True,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`].
|
||||
|
||||
Additionally remaps checkpoint state-dict keys from the upstream openpi naming convention before
|
||||
loading them, and defaults `strict` to `True` rather than `False`.
|
||||
"""
|
||||
"""Override the from_pretrained method to handle key remapping and display important disclaimer."""
|
||||
print(
|
||||
"The PI0Fast model is a direct port of the OpenPI implementation. \n"
|
||||
"This implementation follows the original OpenPI structure for compatibility. \n"
|
||||
@@ -917,6 +912,7 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
self, state_dict, model_config
|
||||
): # see openpi `BaseModelConfig, _fix_pytorch_state_dict_keys`
|
||||
"""Fix state dict keys to match current model architecture."""
|
||||
|
||||
fixed_state_dict = {}
|
||||
|
||||
for key, value in state_dict.items():
|
||||
@@ -940,13 +936,10 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
return fixed_state_dict
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.parameters()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
"""Reset internal state - called when environment resets."""
|
||||
self._action_queue = deque(maxlen=self.config.n_action_steps)
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
@@ -1035,12 +1028,13 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
return images, img_masks
|
||||
|
||||
def prepare_action(self, batch):
|
||||
"""Zero-pad the target action to `config.max_action_dim`."""
|
||||
"""Pad action"""
|
||||
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
|
||||
return actions
|
||||
|
||||
def _paligemma_tokens_to_act_tokens(self, tokens: torch.Tensor) -> torch.Tensor:
|
||||
"""Converts PaliGemma tokens back to action tokens (inverse of _act_tokens_to_paligemma_tokens).
|
||||
"""
|
||||
Converts PaliGemma tokens back to action tokens (inverse of _act_tokens_to_paligemma_tokens).
|
||||
|
||||
Args:
|
||||
tokens: PaliGemma token IDs
|
||||
@@ -1053,21 +1047,17 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
def decode_actions_with_fast(
|
||||
self, token_ids: list[int], time_horizon: int, action_dim: int, relaxed_decoding: bool = True
|
||||
) -> np.ndarray:
|
||||
"""Decode action token IDs back to continuous action values using the FAST tokenizer.
|
||||
"""
|
||||
Decodes action token IDs back to continuous action values using the FAST tokenizer.
|
||||
|
||||
Args:
|
||||
token_ids (`list[int]`):
|
||||
List of token IDs to decode.
|
||||
time_horizon (`int`):
|
||||
The number of timesteps for actions.
|
||||
action_dim (`int`):
|
||||
The dimensionality of each action.
|
||||
relaxed_decoding (`bool`, *optional*, defaults to `True`):
|
||||
Whether to allow decoded DCT coefficient sequences whose length does not exactly match
|
||||
`time_horizon * action_dim`, truncating or zero-padding them to fit.
|
||||
token_ids: List of token IDs to decode.
|
||||
time_horizon: The number of timesteps for actions.
|
||||
action_dim: The dimensionality of each action.
|
||||
relaxed_decoding: Whether to use relaxed decoding (allows partial sequences).
|
||||
|
||||
Returns:
|
||||
`np.ndarray`: The decoded actions.
|
||||
A numpy array representing the decoded actions.
|
||||
"""
|
||||
decoded_actions = []
|
||||
|
||||
@@ -1111,23 +1101,20 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
return np.stack(decoded_actions)
|
||||
|
||||
def detokenize_actions(self, tokens: torch.Tensor, action_horizon: int, action_dim: int) -> torch.Tensor:
|
||||
"""Detokenize action tokens back to continuous actions.
|
||||
"""
|
||||
Detokenizes action tokens back to continuous actions.
|
||||
|
||||
Converts predicted action tokens from the model back to continuous action values using the FAST
|
||||
tokenizer: converts from PaliGemma token space to action token space, then decodes to continuous
|
||||
values using DCT decoding.
|
||||
This method converts predicted action tokens from the model back to continuous action values
|
||||
using the FAST tokenizer. It handles the conversion from PaliGemma token space to action token
|
||||
space, then decodes the action tokens to continuous values using DCT decoding.
|
||||
|
||||
Args:
|
||||
tokens (`torch.Tensor`):
|
||||
The tokenized model output, of shape `(B, seq_len)` or `(seq_len,)`.
|
||||
action_horizon (`int`):
|
||||
The number of timesteps for actions.
|
||||
action_dim (`int`):
|
||||
The dimensionality of each action.
|
||||
tokens: The input tensor of tokenized outputs. Shape: (B, seq_len) or (seq_len,)
|
||||
action_horizon: The number of timesteps for actions.
|
||||
action_dim: The dimensionality of each action.
|
||||
|
||||
Returns:
|
||||
`torch.Tensor`: The continuous action tensor, of shape `(B, action_horizon, action_dim)` or
|
||||
`(action_horizon, action_dim)`.
|
||||
The continuous action tensor. Shape: (B, action_horizon, action_dim) or (action_horizon, action_dim)
|
||||
"""
|
||||
if self.action_tokenizer is None or self._paligemma_tokenizer is None:
|
||||
raise ValueError(
|
||||
@@ -1203,9 +1190,7 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Pops one action off an internal
|
||||
queue, refilling the queue by calling `predict_action_chunk` whenever it is empty.
|
||||
"""
|
||||
"""Select a single action given environment observations."""
|
||||
assert not self._rtc_enabled(), (
|
||||
"RTC is not supported for select_action, use it with predict_action_chunk"
|
||||
)
|
||||
@@ -1222,10 +1207,7 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Autoregressively decodes
|
||||
discrete FAST action tokens (optionally with a key/value cache, see `config.use_kv_cache`) and
|
||||
detokenizes them into a continuous action chunk.
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
self.eval()
|
||||
# Prepare inputs
|
||||
images, img_masks = self._preprocess_images(batch)
|
||||
@@ -1269,17 +1251,8 @@ class PI0FastPolicy(PreTrainedPolicy):
|
||||
return continuous_actions
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the cross-entropy loss between
|
||||
the model's predicted and target discrete FAST action tokens.
|
||||
"""Run the batch through the model and compute the loss for training."""
|
||||
|
||||
Args:
|
||||
batch (`dict[str, Tensor]`):
|
||||
A batch of preprocessed, normalized observation/action tensors, as produced by this
|
||||
policy's preprocessor pipeline. Must contain FAST action tokens and their mask.
|
||||
|
||||
Returns:
|
||||
`tuple[Tensor, dict]`: The loss and a dict of logging-friendly loss statistics.
|
||||
"""
|
||||
# Prepare inputs
|
||||
images, img_masks = self._preprocess_images(batch)
|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@ from .configuration_pi0_fast import PI0FastConfig
|
||||
@ProcessorStepRegistry.register(name="pi0_fast_prepare_state_tokenizer_processor_step")
|
||||
@dataclass
|
||||
class Pi0FastPrepareStateAndLanguageTokenizerProcessorStep(ProcessorStep):
|
||||
"""Processor step to prepare the state and tokenize the language input."""
|
||||
"""
|
||||
Processor step to prepare the state and tokenize the language input.
|
||||
"""
|
||||
|
||||
max_state_dim: int = 32
|
||||
task_key: str = "task"
|
||||
@@ -81,7 +83,9 @@ class Pi0FastPrepareStateAndLanguageTokenizerProcessorStep(ProcessorStep):
|
||||
def transform_features(
|
||||
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
|
||||
) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
|
||||
"""This step does not alter the feature definitions."""
|
||||
"""
|
||||
This step does not alter the feature definitions.
|
||||
"""
|
||||
return features
|
||||
|
||||
|
||||
@@ -92,7 +96,8 @@ def make_pi0_fast_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the PI0Fast policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the PI0Fast policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -107,8 +112,10 @@ def make_pi0_fast_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`PI0FastConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the PI0Fast policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
preprocessor_kwargs: Additional arguments for the pre-processor pipeline.
|
||||
postprocessor_kwargs: Additional arguments for the post-processor pipeline.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
|
||||
@@ -54,27 +54,12 @@ _SINGLE_FILE_SHARD_SIZE = "1TB"
|
||||
|
||||
|
||||
class ActionSelectKwargs(TypedDict, total=False):
|
||||
"""Extra keyword arguments accepted by `select_action`/`predict_action_chunk`.
|
||||
|
||||
**Attributes**:
|
||||
- **noise** (`Tensor | None`) -- Optional pre-sampled noise, for policies whose action generation
|
||||
is stochastic (e.g. diffusion/flow-matching policies), used in place of freshly sampled noise.
|
||||
"""
|
||||
|
||||
noise: Tensor | None
|
||||
|
||||
|
||||
class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
"""Base class for policy models.
|
||||
|
||||
Subclasses must define `config_class` and `name`, and implement `forward`, `predict_action_chunk`,
|
||||
`select_action`, `get_optim_params`, and `reset`. See `docs/source/writing_docstrings.mdx` for the
|
||||
concrete-subclass documentation pattern (config dataclass + this contract's deviations only).
|
||||
|
||||
**Attributes**:
|
||||
- **config_class** (`type[PreTrainedConfig]`) -- The config class this policy expects.
|
||||
- **name** (`str`) -- The registered name of this policy (matches its config's
|
||||
`draccus.ChoiceRegistry` name).
|
||||
"""
|
||||
Base class for policy models.
|
||||
"""
|
||||
|
||||
config_class: None
|
||||
@@ -97,11 +82,6 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
_cp_plan: ClassVar[dict[str, Any] | None] = None
|
||||
|
||||
def __init__(self, config: PreTrainedConfig, *inputs, **kwargs):
|
||||
"""Store `config` on `self.config`. Subclasses build their model in their own `__init__`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `config` is not a `PreTrainedConfig` instance.
|
||||
"""
|
||||
super().__init__()
|
||||
if not isinstance(config, PreTrainedConfig):
|
||||
raise ValueError(
|
||||
@@ -112,11 +92,6 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
self.config = config
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
"""Enforce that every concrete subclass defines `config_class` and `name`.
|
||||
|
||||
Raises:
|
||||
TypeError: If `cls` doesn't define `config_class` or `name`.
|
||||
"""
|
||||
super().__init_subclass__(**kwargs)
|
||||
if not getattr(cls, "config_class", None):
|
||||
raise TypeError(f"Class {cls.__name__} must define 'config_class'")
|
||||
@@ -167,36 +142,9 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
strict: bool = False,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
"""Instantiate the policy from `config` and load its safetensors weights.
|
||||
|
||||
"""
|
||||
The policy is set in evaluation mode by default using `policy.eval()` (dropout modules are
|
||||
deactivated). To train it, you should first set it back in training mode with `policy.train()`.
|
||||
|
||||
Args:
|
||||
pretrained_name_or_path (str | Path): Either the `repo_id` of a model hosted on the Hub, or a
|
||||
path to a directory containing weights saved using `save_pretrained`.
|
||||
config (PreTrainedConfig | None, *optional*): The policy config to use. If `None`, resolved
|
||||
from `pretrained_name_or_path` via `PreTrainedConfig.from_pretrained`.
|
||||
force_download (bool, *optional*, defaults to `False`): Whether to force (re-)downloading the
|
||||
files from the Hub, overriding the existing cache.
|
||||
resume_download (bool | None, *optional*): Deprecated; ignored by the underlying Hub client.
|
||||
proxies (dict | None, *optional*): A dictionary of proxy servers to use by protocol or endpoint.
|
||||
token (str | bool | None, *optional*): The token to use as HTTP bearer authorization for
|
||||
remote files. By default, uses the token cached by `huggingface-cli login`.
|
||||
cache_dir (str | Path | None, *optional*): Path to the folder where cached files are stored.
|
||||
local_files_only (bool, *optional*, defaults to `False`): If `True`, avoid downloading the
|
||||
file and return the path to the local cached file if it exists.
|
||||
revision (str | None, *optional*): Revision on the Hub: a branch name, git tag, or commit id.
|
||||
strict (bool, *optional*, defaults to `False`): Whether to require an exact match between the
|
||||
checkpoint's and the instantiated model's parameter keys.
|
||||
kwargs: Forwarded to `config`'s resolution (when `config` is `None`) and to the policy's
|
||||
constructor.
|
||||
|
||||
Returns:
|
||||
T: The loaded policy, in eval mode, on `config.device`.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the weights file isn't found locally or on the Hub.
|
||||
"""
|
||||
if config is None:
|
||||
config = PreTrainedConfig.from_pretrained(
|
||||
@@ -249,10 +197,8 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_optim_params(self) -> dict:
|
||||
"""Returns the policy-specific parameters dict to be passed on to the optimizer.
|
||||
|
||||
Returns:
|
||||
dict: The policy-specific parameters dict to be passed on to the optimizer.
|
||||
"""
|
||||
Returns the policy-specific parameters dict to be passed on to the optimizer.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -271,11 +217,10 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
# TODO(aliberts, rcadene): split into 'forward' and 'compute_loss'?
|
||||
@abc.abstractmethod
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
||||
"""Compute the training loss for a batch of normalized observations and actions.
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation/action tensors,
|
||||
as produced by this policy's preprocessor pipeline.
|
||||
batch (dict[str, Tensor]): _description_
|
||||
|
||||
Returns:
|
||||
tuple[Tensor, dict | None]: The loss and potentially other information. Apart from the loss which
|
||||
@@ -289,13 +234,6 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
|
||||
Child classes using action chunking should use this method within `select_action` to form the action chunk
|
||||
cached for selection.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation tensors.
|
||||
kwargs: See `ActionSelectKwargs`.
|
||||
|
||||
Returns:
|
||||
Tensor: The predicted action chunk.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -305,13 +243,6 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
|
||||
When the model uses a history of observations, or outputs a sequence of actions, this method deals
|
||||
with caching.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation tensors.
|
||||
kwargs: See `ActionSelectKwargs`.
|
||||
|
||||
Returns:
|
||||
Tensor: The single action to execute next.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -360,7 +291,8 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
peft_config=None,
|
||||
peft_cli_overrides: dict | None = None,
|
||||
) -> PreTrainedPolicy:
|
||||
"""Wrap this policy with PEFT adapters for parameter-efficient fine-tuning.
|
||||
"""
|
||||
Wrap this policy with PEFT adapters for parameter-efficient fine-tuning.
|
||||
|
||||
This method is the single entry point for PEFT integration. Subclasses should
|
||||
override `_get_default_peft_targets()` to provide default target modules, and
|
||||
@@ -404,7 +336,8 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
return peft_model
|
||||
|
||||
def _get_default_peft_targets(self) -> dict[str, any] | None:
|
||||
"""Return default PEFT target modules for this policy.
|
||||
"""
|
||||
Return default PEFT target modules for this policy.
|
||||
|
||||
Override this in subclasses to provide policy-specific defaults. These defaults
|
||||
are PEFT-method agnostic - they only specify which modules to target.
|
||||
@@ -413,7 +346,8 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
return None
|
||||
|
||||
def _validate_peft_config(self, peft_config) -> None:
|
||||
"""Validate the PEFT configuration for this policy.
|
||||
"""
|
||||
Validate the PEFT configuration for this policy.
|
||||
|
||||
Override this in subclasses to add policy-specific validation or warnings.
|
||||
The default implementation checks that a pretrained_path exists.
|
||||
@@ -431,7 +365,8 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
)
|
||||
|
||||
def _preprocess_peft_cli_overrides(self, cli_overrides: dict, peft_method_type) -> dict:
|
||||
"""Preprocess CLI overrides: rename keys and handle method-specific init_type.
|
||||
"""
|
||||
Preprocess CLI overrides: rename keys and handle method-specific init_type.
|
||||
|
||||
Args:
|
||||
cli_overrides: Dict of CLI options (will be copied, not mutated).
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Real Time Chunking (RTC) and Bidirectional Decoding (BID) configuration classes.
|
||||
"""
|
||||
Real Time Chunking (RTC) and Bidirectional Decoding (BID) configuration classes.
|
||||
|
||||
Based on:
|
||||
- Real Time Chunking: https://www.physicalintelligence.company/research/real_time_chunking
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Real-Time Chunking (RTC) implementation for LeRobot.
|
||||
"""
|
||||
Real-Time Chunking (RTC) implementation for LeRobot.
|
||||
|
||||
Based on Physical Intelligence's Kinetix implementation:
|
||||
https://github.com/Physical-Intelligence/real-time-chunking-kinetix/blob/main/src/model.py#L214
|
||||
@@ -42,7 +43,6 @@ class RTCProcessor:
|
||||
"""
|
||||
|
||||
def __init__(self, rtc_config: RTCConfig):
|
||||
"""Create the processor; starts a debug `Tracker` when `rtc_config.debug` is set."""
|
||||
self.rtc_config = rtc_config
|
||||
|
||||
self.tracker = None
|
||||
@@ -158,6 +158,7 @@ class RTCProcessor:
|
||||
Reference:
|
||||
https://www.physicalintelligence.company/download/real_time_chunking.pdf
|
||||
"""
|
||||
|
||||
# In the original implementation, the time goes from 0 to 1 and
|
||||
# In our implementation, the time goes from 1 to 0
|
||||
# So we need to invert the time
|
||||
@@ -248,16 +249,6 @@ class RTCProcessor:
|
||||
return result
|
||||
|
||||
def get_prefix_weights(self, start, end, total):
|
||||
"""Build the prefix-attention weight vector, per `rtc_config.prefix_attention_schedule`.
|
||||
|
||||
Args:
|
||||
start: The step at which the weight begins ramping down from 1.0 (`ZEROS`/`LINEAR`/`EXP`).
|
||||
end: The step by which the weight reaches 0.0 (`ONES`/`LINEAR`/`EXP`).
|
||||
total: The length of the returned weight vector.
|
||||
|
||||
Returns:
|
||||
A `(total,)` tensor of weights in `[0, 1]`.
|
||||
"""
|
||||
start = min(start, end)
|
||||
|
||||
if self.rtc_config.prefix_attention_schedule == RTCAttentionSchedule.ZEROS:
|
||||
|
||||
@@ -24,130 +24,6 @@ from ..rtc.configuration_rtc import RTCConfig
|
||||
@PreTrainedConfig.register_subclass("smolvla")
|
||||
@dataclass
|
||||
class SmolVLAConfig(PreTrainedConfig):
|
||||
"""Configuration class for the SmolVLA flow-matching vision-language-action policy.
|
||||
|
||||
SmolVLA pairs a SmolVLM2 vision-language backbone with a smaller flow-matching action expert that
|
||||
cross-attends (or self-attends, depending on `attention_mode`) into the VLM's hidden states to
|
||||
generate action chunks.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Inferred from the
|
||||
dataset when left empty.
|
||||
device (`str | None`, *optional*):
|
||||
Device to run the model on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Auto-detected when
|
||||
`None`.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether the policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Repository ID to push the trained policy to on the Hub.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub repository.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to attach to the policy's Hub repository.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Repo ID on the Hub or local directory to load pretrained weights from. The policy is
|
||||
initialized from scratch when `None`.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 50):
|
||||
Number of action steps predicted per model invocation.
|
||||
n_action_steps (`int`, *optional*, defaults to 50):
|
||||
Number of predicted action steps actually executed in the environment before predicting a new
|
||||
chunk. Must not exceed `chunk_size`.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Mapping from feature type (`"VISUAL"`, `"STATE"`, `"ACTION"`) to the `NormalizationMode` used
|
||||
for it.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the observation state vector is zero-padded to when shorter.
|
||||
max_action_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the action vector is zero-padded to when shorter.
|
||||
resize_imgs_with_padding (`tuple[int, int]`, *optional*, defaults to `(512, 512)`):
|
||||
Target `(width, height)` images are resized (with aspect-ratio-preserving padding) to before
|
||||
being fed to the vision encoder.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of empty (zero-padded) camera views to add, e.g. for the aloha_sim variants that expect
|
||||
extra wrist cameras.
|
||||
adapt_to_pi_aloha (`bool`, *optional*, defaults to `False`):
|
||||
Whether to convert joint and gripper values from the standard Aloha space to the space used by
|
||||
the pi internal runtime the base model was trained with.
|
||||
use_delta_joint_actions_aloha (`bool`, *optional*, defaults to `False`):
|
||||
Whether to convert joint dimensions (gripper excluded) to values relative to the current state
|
||||
before passing them to the model. Not yet ported in LeRobot; raises if enabled.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 48):
|
||||
Maximum token length for the language tokenizer.
|
||||
num_steps (`int`, *optional*, defaults to 10):
|
||||
Number of flow-matching denoising steps performed at inference time.
|
||||
use_cache (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use a key/value cache in the VLM and action expert for faster inference.
|
||||
freeze_vision_encoder (`bool`, *optional*, defaults to `True`):
|
||||
Whether to freeze the vision encoder's weights during training.
|
||||
train_expert_only (`bool`, *optional*, defaults to `True`):
|
||||
Whether to freeze the VLM and train only the action expert.
|
||||
train_state_proj (`bool`, *optional*, defaults to `True`):
|
||||
Whether to train the state projection layer.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Peak learning rate for the AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`):
|
||||
AdamW `(beta1, beta2)` coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
AdamW epsilon term for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-10):
|
||||
AdamW weight decay coefficient.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 10):
|
||||
Maximum gradient norm for clipping.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the learning rate scheduler.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Learning rate the scheduler decays to at the end of `scheduler_decay_steps`.
|
||||
vlm_model_name (`str`, *optional*, defaults to `"HuggingFaceTB/SmolVLM2-500M-Video-Instruct"`):
|
||||
Hub identifier of the SmolVLM2 backbone to use.
|
||||
load_vlm_weights (`bool`, *optional*, defaults to `False`):
|
||||
Whether to load the VLM's pretrained weights. Set `False` when training the expert from
|
||||
scratch, `True` when initializing from pretrained SmolVLA weights.
|
||||
add_image_special_tokens (`bool`, *optional*, defaults to `False`):
|
||||
Whether to surround image features with special image tokens.
|
||||
attention_mode (`str`, *optional*, defaults to `"cross_attn"`):
|
||||
How the action expert attends into the VLM's hidden states.
|
||||
prefix_length (`int`, *optional*, defaults to -1):
|
||||
Fixed length the VLM prefix (image and language tokens) is padded to. `-1` disables padding.
|
||||
pad_language_to (`str`, *optional*, defaults to `"longest"`):
|
||||
Padding strategy for the language tokenizer, e.g. `"longest"` or `"max_length"`.
|
||||
num_expert_layers (`int`, *optional*, defaults to -1):
|
||||
Number of transformer layers in the action expert. A value `<= 0` uses the same number of
|
||||
layers as the VLM; otherwise the expert has fewer layers.
|
||||
num_vlm_layers (`int`, *optional*, defaults to 16):
|
||||
Number of layers used from the VLM backbone (the first `num_vlm_layers` layers).
|
||||
self_attn_every_n_layers (`int`, *optional*, defaults to 2):
|
||||
Interleave a self-attention layer every `self_attn_every_n_layers` expert layers.
|
||||
expert_width_multiplier (`float`, *optional*, defaults to 0.75):
|
||||
The action expert's hidden size, expressed as a multiplier of the VLM's hidden size.
|
||||
min_period (`float`, *optional*, defaults to 0.004):
|
||||
Minimum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
max_period (`float`, *optional*, defaults to 4.0):
|
||||
Maximum period of the sinusoidal positional encoding used to embed the flow-matching timestep.
|
||||
rtc_config (`RTCConfig | None`, *optional*):
|
||||
Real-Time Chunking configuration. `None` disables RTC inference.
|
||||
compile_model (`bool`, *optional*, defaults to `False`):
|
||||
Whether to compile the model with `torch.compile`.
|
||||
compile_mode (`str`, *optional*, defaults to `"max-autotune"`):
|
||||
The `torch.compile` mode to use when `compile_model` is enabled.
|
||||
"""
|
||||
|
||||
# Input / output structure.
|
||||
n_obs_steps: int = 1
|
||||
chunk_size: int = 50
|
||||
@@ -231,9 +107,9 @@ class SmolVLAConfig(PreTrainedConfig):
|
||||
compile_mode: str = "max-autotune" # Torch compile mode
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the SmolVLM backbone configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
"""Input validation (not exhaustive)."""
|
||||
if self.n_action_steps > self.chunk_size:
|
||||
raise ValueError(
|
||||
f"The chunk size is the upper bound for the number of action steps per model invocation. Got "
|
||||
@@ -245,7 +121,6 @@ class SmolVLAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""Validate and set up input/output features."""
|
||||
for i in range(self.empty_cameras):
|
||||
key = f"{OBS_IMAGES}.empty_camera_{i}"
|
||||
empty_camera = PolicyFeature(
|
||||
@@ -255,7 +130,6 @@ class SmolVLAConfig(PreTrainedConfig):
|
||||
self.input_features[key] = empty_camera
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -265,7 +139,6 @@ class SmolVLAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -275,15 +148,12 @@ class SmolVLAConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return [0]
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""SmolVLA:
|
||||
"""
|
||||
SmolVLA:
|
||||
|
||||
[Paper](https://huggingface.co/papers/2506.01844)
|
||||
|
||||
@@ -139,17 +140,12 @@ def aloha_gripper_from_angular_inv(value):
|
||||
|
||||
|
||||
class SmolVLAPolicy(PreTrainedPolicy):
|
||||
"""SmolVLA vision-language-action policy: a `VLAFlowMatching` model (SmolVLM2 backbone plus a
|
||||
flow-matching action expert) wrapped for training and inference within LeRobot.
|
||||
"""
|
||||
"""Wrapper class around VLAFlowMatching model to train and run inference within LeRobot."""
|
||||
|
||||
config_class = SmolVLAConfig
|
||||
name = "smolvla"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.supports_rtc`]. SmolVLA implements Real-Time
|
||||
Chunking inference.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
@@ -157,11 +153,12 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
config: SmolVLAConfig,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the underlying `VLAFlowMatching` model from `config`.
|
||||
|
||||
Args:
|
||||
config (`SmolVLAConfig`): Policy configuration class instance.
|
||||
"""
|
||||
Args:
|
||||
config: Policy configuration class instance or None, in which case the default instantiation of
|
||||
the configuration class is used.
|
||||
"""
|
||||
|
||||
require_package("transformers", extra="smolvla")
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -171,9 +168,7 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
"""This should be called whenever the environment is reset."""
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
}
|
||||
@@ -195,7 +190,6 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
model_value.rtc_processor = self.rtc_processor
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.parameters()
|
||||
|
||||
def _get_action_chunk(
|
||||
@@ -238,9 +232,6 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
def predict_action_chunk(
|
||||
self, batch: dict[str, Tensor], noise: Tensor | None = None, **kwargs: Unpack[ActionSelectKwargs]
|
||||
) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Runs the flow-matching
|
||||
sampler (`config.num_steps` denoising steps) to generate the chunk.
|
||||
"""
|
||||
self.eval()
|
||||
|
||||
batch = self._prepare_batch(batch)
|
||||
@@ -253,9 +244,13 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
def select_action(
|
||||
self, batch: dict[str, Tensor], noise: Tensor | None = None, **kwargs: Unpack[ActionSelectKwargs]
|
||||
) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Pops one action off an internal
|
||||
queue, refilling the queue by calling `predict_action_chunk` whenever it is empty.
|
||||
"""Select a single action given environment observations.
|
||||
|
||||
This method wraps `select_actions` in order to return one action at a time for execution in the
|
||||
environment. It works by managing the actions in a queue and only calling `select_actions` when the
|
||||
queue is empty.
|
||||
"""
|
||||
|
||||
assert not self._rtc_enabled(), (
|
||||
"RTC is not supported for select_action, use it with predict_action_chunk"
|
||||
)
|
||||
@@ -282,23 +277,15 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
def forward(
|
||||
self, batch: dict[str, Tensor], noise=None, time=None, reduction: str = "mean"
|
||||
) -> dict[str, Tensor]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the flow-matching loss between
|
||||
the model's predicted and target velocity fields.
|
||||
"""Do a full training forward pass to compute the loss.
|
||||
|
||||
Args:
|
||||
batch (`dict[str, Tensor]`):
|
||||
A batch of preprocessed, normalized observation/action tensors, as produced by this
|
||||
policy's preprocessor pipeline.
|
||||
noise (`Tensor | None`, *optional*):
|
||||
Pre-sampled noise for the flow-matching objective. Freshly sampled when `None`.
|
||||
time (`Tensor | None`, *optional*):
|
||||
Pre-sampled flow-matching timestep. Freshly sampled when `None`.
|
||||
reduction (`str`, *optional*, defaults to `"mean"`):
|
||||
How to reduce the per-element loss. `"mean"` returns a scalar mean loss; `"none"` returns
|
||||
per-sample losses of shape `(batch_size,)`, e.g. for RA-BC weighting.
|
||||
|
||||
Returns:
|
||||
`tuple[Tensor, dict]`: The loss and a dict of logging-friendly loss statistics.
|
||||
batch: Training batch containing observations and actions.
|
||||
noise: Optional noise tensor for flow matching.
|
||||
time: Optional time tensor for flow matching.
|
||||
reduction: How to reduce the loss. Options:
|
||||
- "mean": Return scalar mean loss (default, backward compatible)
|
||||
- "none": Return per-sample losses of shape (batch_size,) for RA-BC weighting
|
||||
"""
|
||||
if self.config.adapt_to_pi_aloha:
|
||||
batch[OBS_STATE] = self._pi_aloha_decode_state(batch[OBS_STATE])
|
||||
@@ -420,13 +407,13 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
return actions
|
||||
|
||||
def prepare_state(self, batch):
|
||||
"""Take the latest observation state and zero-pad it to `config.max_state_dim`."""
|
||||
"""Pad state"""
|
||||
state = batch[OBS_STATE][:, -1, :] if batch[OBS_STATE].ndim > 2 else batch[OBS_STATE]
|
||||
state = pad_vector(state, self.config.max_state_dim)
|
||||
return state
|
||||
|
||||
def prepare_action(self, batch):
|
||||
"""Zero-pad the target action to `config.max_action_dim`."""
|
||||
"""Pad action"""
|
||||
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
|
||||
return actions
|
||||
|
||||
@@ -454,12 +441,13 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
|
||||
|
||||
def pad_tensor(tensor, max_len, pad_value=0):
|
||||
"""Efficiently pads a tensor along sequence dimension to match max_len.
|
||||
"""
|
||||
Efficiently pads a tensor along sequence dimension to match max_len.
|
||||
|
||||
Args:
|
||||
tensor (torch.Tensor): Shape (B, L, ...) or (B, L).
|
||||
max_len (int): Fixed sequence length.
|
||||
pad_value (int/float, *optional*, defaults to 0): Value for padding.
|
||||
pad_value (int/float): Value for padding.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Shape (B, max_len, ...) or (B, max_len).
|
||||
@@ -476,7 +464,8 @@ def pad_tensor(tensor, max_len, pad_value=0):
|
||||
|
||||
|
||||
class VLAFlowMatching(nn.Module):
|
||||
"""SmolVLA
|
||||
"""
|
||||
SmolVLA
|
||||
|
||||
[Paper]()
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ def make_smolvla_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the SmolVLA policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the SmolVLA policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -52,12 +53,13 @@ def make_smolvla_pre_post_processors(
|
||||
2. Unnormalizing the output actions to their original scale.
|
||||
|
||||
Args:
|
||||
config (`SmolVLAConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the SmolVLA policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
"""
|
||||
|
||||
steps = make_default_policy_processor_steps(config, dataset_stats)
|
||||
|
||||
input_steps = [
|
||||
|
||||
@@ -32,125 +32,65 @@ class TDMPCConfig(PreTrainedConfig):
|
||||
Those are: `input_features`, `output_features`, and perhaps `max_random_shift_ratio`.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back). TD-MPC only supports a value of 1; anything else raises in
|
||||
`__post_init__`.
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy
|
||||
is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
n_action_repeats (`int`, *optional*, defaults to 2):
|
||||
The number of times to repeat the action returned by the planning. (hint: Google action
|
||||
repeats in Q-learning or ask your favorite chatbot.)
|
||||
horizon (`int`, *optional*, defaults to 5):
|
||||
Horizon for model predictive control.
|
||||
n_action_steps (`int`, *optional*, defaults to 1):
|
||||
Number of action steps to take from the plan given by model predictive control. This is an
|
||||
alternative to using action repeats. If this is set to more than 1, then we require
|
||||
n_action_repeats: The number of times to repeat the action returned by the planning. (hint: Google
|
||||
action repeats in Q-learning or ask your favorite chatbot)
|
||||
horizon: Horizon for model predictive control.
|
||||
n_action_steps: Number of action steps to take from the plan given by model predictive control. This
|
||||
is an alternative to using action repeats. If this is set to more than 1, then we require
|
||||
`n_action_repeats == 1`, `use_mpc == True` and `n_action_steps <= horizon`. Note that this
|
||||
approach of using multiple steps from the plan is not in the original implementation.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to identity normalization for visual, state, and environment features, and
|
||||
min/max normalization for the action, which TD-MPC assumes lies in `[-1, 1]`.
|
||||
image_encoder_hidden_dim (`int`, *optional*, defaults to 32):
|
||||
Number of channels for the convolutional layers used for image encoding.
|
||||
state_encoder_hidden_dim (`int`, *optional*, defaults to 256):
|
||||
Hidden dimension for MLP used for state vector encoding.
|
||||
latent_dim (`int`, *optional*, defaults to 50):
|
||||
Observation's latent embedding dimension.
|
||||
q_ensemble_size (`int`, *optional*, defaults to 5):
|
||||
Number of Q function estimators to use in an ensemble for uncertainty estimation.
|
||||
mlp_dim (`int`, *optional*, defaults to 512):
|
||||
Hidden dimension of MLPs used for modelling the dynamics encoder, reward function, policy (π),
|
||||
Q ensemble, and V.
|
||||
discount (`float`, *optional*, defaults to 0.9):
|
||||
Discount factor (γ) to use for the reinforcement learning formalism.
|
||||
use_mpc (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use model predictive control. The alternative is to just sample the policy model
|
||||
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents
|
||||
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents
|
||||
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
||||
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
||||
image_encoder_hidden_dim: Number of channels for the convolutional layers used for image encoding.
|
||||
state_encoder_hidden_dim: Hidden dimension for MLP used for state vector encoding.
|
||||
latent_dim: Observation's latent embedding dimension.
|
||||
q_ensemble_size: Number of Q function estimators to use in an ensemble for uncertainty estimation.
|
||||
mlp_dim: Hidden dimension of MLPs used for modelling the dynamics encoder, reward function, policy
|
||||
(π), Q ensemble, and V.
|
||||
discount: Discount factor (γ) to use for the reinforcement learning formalism.
|
||||
use_mpc: Whether to use model predictive control. The alternative is to just sample the policy model
|
||||
(π) for each step.
|
||||
cem_iterations (`int`, *optional*, defaults to 6):
|
||||
Number of iterations for the MPPI/CEM loop in MPC.
|
||||
max_std (`float`, *optional*, defaults to 2.0):
|
||||
Maximum standard deviation for actions sampled from the gaussian PDF in CEM.
|
||||
min_std (`float`, *optional*, defaults to 0.05):
|
||||
Minimum standard deviation for noise applied to actions sampled from the policy model (π).
|
||||
cem_iterations: Number of iterations for the MPPI/CEM loop in MPC.
|
||||
max_std: Maximum standard deviation for actions sampled from the gaussian PDF in CEM.
|
||||
min_std: Minimum standard deviation for noise applied to actions sampled from the policy model (π).
|
||||
Doubles up as the minimum standard deviation for actions sampled from the gaussian PDF in CEM.
|
||||
n_gaussian_samples (`int`, *optional*, defaults to 512):
|
||||
Number of samples to draw from the gaussian distribution every CEM iteration. Must be
|
||||
non-zero.
|
||||
n_pi_samples (`int`, *optional*, defaults to 51):
|
||||
Number of samples to draw from the policy / world model rollout every CEM iteration. Can be
|
||||
zero.
|
||||
uncertainty_regularizer_coeff (`float`, *optional*, defaults to 1.0):
|
||||
Coefficient for the uncertainty regularization used when estimating trajectory values (this is
|
||||
the λ coefficient in eqn 4 of FOWM).
|
||||
n_elites (`int`, *optional*, defaults to 50):
|
||||
The number of elite samples to use for updating the gaussian parameters every CEM iteration.
|
||||
elite_weighting_temperature (`float`, *optional*, defaults to 0.5):
|
||||
The temperature to use for softmax weighting (by trajectory value) of the elites, when
|
||||
updating the gaussian parameters for CEM.
|
||||
gaussian_mean_momentum (`float`, *optional*, defaults to 0.1):
|
||||
Momentum (α) used for EMA updates of the mean parameter μ of the gaussian parameters optimized
|
||||
in CEM. Updates are calculated as μ⁻ ← αμ⁻ + (1-α)μ.
|
||||
max_random_shift_ratio (`float`, *optional*, defaults to 0.0476):
|
||||
Maximum random shift (as a proportion of the image size) to apply to the image(s) (in units of
|
||||
pixels) for training-time augmentation. If set to 0, no such augmentation is applied. Note
|
||||
that the input images are assumed to be square for this augmentation.
|
||||
reward_coeff (`float`, *optional*, defaults to 0.5):
|
||||
Loss weighting coefficient for the reward regression loss.
|
||||
expectile_weight (`float`, *optional*, defaults to 0.9):
|
||||
Weighting (τ) used in expectile regression for the state value function (V). `v_pred <
|
||||
v_target` is weighted by τ and `v_pred >= v_target` is weighted by `(1-τ)`. τ is expected to
|
||||
be in `[0, 1]`. Setting τ closer to 1 results in a more "optimistic" V. This is sensible to do
|
||||
because `v_target` is obtained by evaluating the learned state-action value functions (Q) with
|
||||
in-sample actions that may not always be optimal.
|
||||
value_coeff (`float`, *optional*, defaults to 0.1):
|
||||
Loss weighting coefficient for both the state-action value (Q) TD loss, and the state value
|
||||
(V) expectile regression loss.
|
||||
consistency_coeff (`float`, *optional*, defaults to 20.0):
|
||||
Loss weighting coefficient for the consistency loss.
|
||||
advantage_scaling (`float`, *optional*, defaults to 3.0):
|
||||
A factor by which the advantages are scaled prior to exponentiation for advantage weighted
|
||||
regression of the policy (π) estimator parameters. Note that the exponentiated advantages are
|
||||
clamped at 100.0.
|
||||
pi_coeff (`float`, *optional*, defaults to 0.5):
|
||||
Loss weighting coefficient for the action regression loss.
|
||||
temporal_decay_coeff (`float`, *optional*, defaults to 0.5):
|
||||
Exponential decay coefficient for decaying the loss coefficient for future time-steps. Hint:
|
||||
each loss computation involves `horizon` steps worth of actions starting from the current
|
||||
time step.
|
||||
target_model_momentum (`float`, *optional*, defaults to 0.995):
|
||||
Momentum (α) used for EMA updates of the target models. Updates are calculated as ϕ ← αϕ +
|
||||
(1-α)θ where ϕ are the parameters of the target model and θ are the parameters of the model
|
||||
being trained.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0003):
|
||||
Learning rate for the Adam optimizer preset.
|
||||
n_gaussian_samples: Number of samples to draw from the gaussian distribution every CEM iteration. Must
|
||||
be non-zero.
|
||||
n_pi_samples: Number of samples to draw from the policy / world model rollout every CEM iteration. Can
|
||||
be zero.
|
||||
uncertainty_regularizer_coeff: Coefficient for the uncertainty regularization used when estimating
|
||||
trajectory values (this is the λ coefficient in eqn 4 of FOWM).
|
||||
n_elites: The number of elite samples to use for updating the gaussian parameters every CEM iteration.
|
||||
elite_weighting_temperature: The temperature to use for softmax weighting (by trajectory value) of the
|
||||
elites, when updating the gaussian parameters for CEM.
|
||||
gaussian_mean_momentum: Momentum (α) used for EMA updates of the mean parameter μ of the gaussian
|
||||
parameters optimized in CEM. Updates are calculated as μ⁻ ← αμ⁻ + (1-α)μ.
|
||||
max_random_shift_ratio: Maximum random shift (as a proportion of the image size) to apply to the
|
||||
image(s) (in units of pixels) for training-time augmentation. If set to 0, no such augmentation
|
||||
is applied. Note that the input images are assumed to be square for this augmentation.
|
||||
reward_coeff: Loss weighting coefficient for the reward regression loss.
|
||||
expectile_weight: Weighting (τ) used in expectile regression for the state value function (V).
|
||||
v_pred < v_target is weighted by τ and v_pred >= v_target is weighted by (1-τ). τ is expected to
|
||||
be in [0, 1]. Setting τ closer to 1 results in a more "optimistic" V. This is sensible to do
|
||||
because v_target is obtained by evaluating the learned state-action value functions (Q) with
|
||||
in-sample actions that may not be always optimal.
|
||||
value_coeff: Loss weighting coefficient for both the state-action value (Q) TD loss, and the state
|
||||
value (V) expectile regression loss.
|
||||
consistency_coeff: Loss weighting coefficient for the consistency loss.
|
||||
advantage_scaling: A factor by which the advantages are scaled prior to exponentiation for advantage
|
||||
weighted regression of the policy (π) estimator parameters. Note that the exponentiated advantages
|
||||
are clamped at 100.0.
|
||||
pi_coeff: Loss weighting coefficient for the action regression loss.
|
||||
temporal_decay_coeff: Exponential decay coefficient for decaying the loss coefficient for future time-
|
||||
steps. Hint: each loss computation involves `horizon` steps worth of actions starting from the
|
||||
current time step.
|
||||
target_model_momentum: Momentum (α) used for EMA updates of the target models. Updates are calculated
|
||||
as ϕ ← αϕ + (1-α)θ where ϕ are the parameters of the target model and θ are the parameters of the
|
||||
model being trained.
|
||||
"""
|
||||
|
||||
# Input / output structure.
|
||||
@@ -207,9 +147,9 @@ class TDMPCConfig(PreTrainedConfig):
|
||||
optimizer_lr: float = 3e-4
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the planning horizon and network configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
"""Input validation (not exhaustive)."""
|
||||
if self.n_gaussian_samples <= 0:
|
||||
raise ValueError(
|
||||
f"The number of gaussian samples for CEM should be non-zero. Got `{self.n_gaussian_samples=}`"
|
||||
@@ -235,15 +175,12 @@ class TDMPCConfig(PreTrainedConfig):
|
||||
raise ValueError("`n_action_steps` must be less than or equal to `horizon`.")
|
||||
|
||||
def get_optimizer_preset(self) -> AdamConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamConfig(lr=self.optimizer_lr)
|
||||
|
||||
def get_scheduler_preset(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return None
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
# There should only be one image key.
|
||||
if len(self.image_features) > 1:
|
||||
raise ValueError(
|
||||
@@ -259,15 +196,12 @@ class TDMPCConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return list(range(self.horizon + 1))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.horizon))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return list(range(self.horizon))
|
||||
|
||||
@@ -68,11 +68,10 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
config: TDMPCConfig,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the world model (and its EMA target copy) from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`TDMPCConfig`):
|
||||
Policy configuration.
|
||||
config: Policy configuration class instance or None, in which case the default instantiation of
|
||||
the configuration class is used.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -86,14 +85,12 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]."""
|
||||
return self.parameters()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Clears the observation and action queues, and the previous CEM mean used to warm-start MPPI/CEM
|
||||
planning in `plan`.
|
||||
"""
|
||||
Clear observation and action queues. Clear previous means for warm starting of MPPI/CEM. Should be
|
||||
called on `env.reset()`
|
||||
"""
|
||||
self._queues = {
|
||||
OBS_STATE: deque(maxlen=1),
|
||||
@@ -109,11 +106,7 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Encodes the observation and, when `use_mpc` is enabled, plans a trajectory with `plan`; otherwise
|
||||
samples a single action from the policy model (π).
|
||||
"""
|
||||
"""Predict a chunk of actions given environment observations."""
|
||||
batch = {key: torch.stack(list(self._queues[key]), dim=1) for key in batch if key in self._queues}
|
||||
|
||||
# Remove the time dimensions as it is not handled yet.
|
||||
@@ -142,11 +135,7 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`, either repeating a single action
|
||||
`n_action_repeats` times or taking `n_action_steps` from the planned trajectory.
|
||||
"""
|
||||
"""Select a single action given environment observations."""
|
||||
# NOTE: for offline evaluation, we have action in the batch, so we need to pop it out
|
||||
if ACTION in batch:
|
||||
batch.pop(ACTION)
|
||||
@@ -176,14 +165,12 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def plan(self, z: Tensor) -> Tensor:
|
||||
"""Plan a sequence of actions using TD-MPC inference (MPPI/CEM over the learned world model).
|
||||
"""Plan sequence of actions using TD-MPC inference.
|
||||
|
||||
Args:
|
||||
z (`Tensor`):
|
||||
`(batch, latent_dim)` tensor for the initial state.
|
||||
|
||||
z: (batch, latent_dim,) tensor for the initial state.
|
||||
Returns:
|
||||
`Tensor`: `(horizon, batch, action_dim)` tensor for the planned trajectory of actions.
|
||||
(horizon, batch, action_dim,) tensor for the planned trajectory of actions.
|
||||
"""
|
||||
device = get_device_from_parameters(self)
|
||||
|
||||
@@ -272,16 +259,13 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def estimate_value(self, z: Tensor, actions: Tensor):
|
||||
"""Estimate the value of a trajectory as per eqn 4 of the FOWM paper.
|
||||
"""Estimates the value of a trajectory as per eqn 4 of the FOWM paper.
|
||||
|
||||
Args:
|
||||
z (`Tensor`):
|
||||
`(batch, latent_dim)` tensor of initial latent states.
|
||||
actions (`Tensor`):
|
||||
`(horizon, batch, action_dim)` tensor of action trajectories.
|
||||
|
||||
z: (batch, latent_dim) tensor of initial latent states.
|
||||
actions: (horizon, batch, action_dim) tensor of action trajectories.
|
||||
Returns:
|
||||
`Tensor`: `(batch,)` tensor of values.
|
||||
(batch,) tensor of values.
|
||||
"""
|
||||
# Initialize return and running discount factor.
|
||||
G, running_discount = 0, 1
|
||||
@@ -324,11 +308,9 @@ class TDMPCPolicy(PreTrainedPolicy):
|
||||
return G
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
"""Run the batch through the model and compute the loss.
|
||||
|
||||
The loss combines a latent-consistency loss, a reward regression loss, TD and expectile losses for
|
||||
the Q/V value functions, and an advantage-weighted regression loss for the policy (π), each
|
||||
weighted by its corresponding `*_coeff` config field.
|
||||
Returns a dictionary with loss as a tensor, and other information as native floats.
|
||||
"""
|
||||
device = get_device_from_parameters(self)
|
||||
|
||||
@@ -631,7 +613,6 @@ class TDMPCTOLD(nn.Module):
|
||||
Args:
|
||||
z: (*, latent_dim) tensor for the current state's latent representation.
|
||||
a: (*, action_dim) tensor for the action to be applied.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
- (*, latent_dim) tensor for the next state's latent representation.
|
||||
@@ -646,7 +627,6 @@ class TDMPCTOLD(nn.Module):
|
||||
Args:
|
||||
z: (*, latent_dim) tensor for the current state's latent representation.
|
||||
a: (*, action_dim) tensor for the action to be applied.
|
||||
|
||||
Returns:
|
||||
(*, latent_dim) tensor for the next state's latent representation.
|
||||
"""
|
||||
@@ -662,7 +642,6 @@ class TDMPCTOLD(nn.Module):
|
||||
Args:
|
||||
z: (*, latent_dim) tensor for the current state's latent representation.
|
||||
std: The standard deviation of the injected noise.
|
||||
|
||||
Returns:
|
||||
(*, action_dim) tensor for the sampled action.
|
||||
"""
|
||||
@@ -677,7 +656,6 @@ class TDMPCTOLD(nn.Module):
|
||||
|
||||
Args:
|
||||
z: (*, latent_dim) tensor for the current state's latent representation.
|
||||
|
||||
Returns:
|
||||
(*,) tensor of estimated state values.
|
||||
"""
|
||||
@@ -710,7 +688,8 @@ class TDMPCObservationEncoder(nn.Module):
|
||||
"""Encode image and/or state vector observations."""
|
||||
|
||||
def __init__(self, config: TDMPCConfig):
|
||||
"""Creates encoders for pixel and/or state modalities.
|
||||
"""
|
||||
Creates encoders for pixel and/or state modalities.
|
||||
TODO(alexander-soare): The original work allows for multiple images by concatenating them along the
|
||||
channel dimension. Re-implement this capability.
|
||||
"""
|
||||
@@ -837,12 +816,12 @@ def flatten_forward_unflatten(fn: Callable[[Tensor], Tensor], image_tensor: Tens
|
||||
"""Helper to temporarily flatten extra dims at the start of the image tensor.
|
||||
|
||||
Args:
|
||||
fn (`Callable`): A callable expecting a 4D `(B, C, H, W)` image tensor.
|
||||
image_tensor (`Tensor`): An image tensor with any number of leading batch-like dims, e.g.
|
||||
`(*, C, H, W)`.
|
||||
|
||||
fn: Callable that the image tensor will be passed to. It should accept (B, C, H, W) and return
|
||||
(B, *), where * is any number of dimensions.
|
||||
image_tensor: An image tensor of shape (**, C, H, W), where ** is any number of dimensions, generally
|
||||
different from *.
|
||||
Returns:
|
||||
A return value from the callable reshaped to `(*, *)`.
|
||||
A return value from the callable reshaped to (**, *).
|
||||
"""
|
||||
if image_tensor.ndim == 4:
|
||||
return fn(image_tensor)
|
||||
|
||||
@@ -34,7 +34,8 @@ def make_tdmpc_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the TDMPC policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the TDMPC policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations.
|
||||
@@ -47,8 +48,8 @@ def make_tdmpc_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`TDMPCConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the TDMPC policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
|
||||
@@ -30,20 +30,6 @@ from lerobot.utils.feature_utils import build_dataset_frame
|
||||
def populate_queues(
|
||||
queues: dict[str, deque], batch: dict[str, torch.Tensor], exclude_keys: list[str] | None = None
|
||||
):
|
||||
"""Push each `batch` entry onto its matching queue, filling a fresh queue by repeating the first value.
|
||||
|
||||
Keys in `batch` with no matching queue in `queues`, or listed in `exclude_keys`, are skipped.
|
||||
|
||||
Args:
|
||||
queues (`dict`): A mapping from key to a fixed-maxlen `deque`, one per observation/action history
|
||||
to track.
|
||||
batch (`dict`): The latest values to push, keyed the same way as `queues`.
|
||||
exclude_keys (`list[str] | None`, *optional*): Keys to skip even if present in both `batch` and
|
||||
`queues`.
|
||||
|
||||
Returns:
|
||||
`queues`, updated in place (also returned for convenience).
|
||||
"""
|
||||
if exclude_keys is None:
|
||||
exclude_keys = []
|
||||
for key in batch:
|
||||
@@ -78,12 +64,12 @@ def get_dtype_from_parameters(module: nn.Module) -> torch.dtype:
|
||||
|
||||
|
||||
def get_output_shape(module: nn.Module, input_shape: tuple) -> tuple:
|
||||
"""Calculates the output shape of a PyTorch module given an input shape.
|
||||
"""
|
||||
Calculates the output shape of a PyTorch module given an input shape.
|
||||
|
||||
Args:
|
||||
module (nn.Module): a PyTorch module
|
||||
input_shape (tuple): A tuple representing the input shape, e.g., `(batch_size, channels, height,
|
||||
width)`.
|
||||
input_shape (tuple): A tuple representing the input shape, e.g., (batch_size, channels, height, width)
|
||||
|
||||
Returns:
|
||||
tuple: The output shape of the module.
|
||||
@@ -125,10 +111,12 @@ def prepare_observation_for_inference(
|
||||
5. Adding task and robot type information to the dictionary.
|
||||
|
||||
Args:
|
||||
observation (`dict`): A dictionary of NumPy arrays keyed by observation name.
|
||||
device (`device`): The torch device to move the resulting tensors to.
|
||||
task (`str | None`, *optional*): The task string to attach to the observation, if any.
|
||||
robot_type (`str | None`, *optional*): The robot type string to attach to the observation, if any.
|
||||
observation: A dictionary mapping observation names (str) to NumPy
|
||||
array data. For images, the format is expected to be (H, W, C).
|
||||
device: The PyTorch device (e.g., 'cpu' or 'cuda') to which the
|
||||
tensors will be moved.
|
||||
task: An optional string identifier for the current task.
|
||||
robot_type: An optional string identifier for the robot being used.
|
||||
|
||||
Returns:
|
||||
A dictionary where values are PyTorch tensors preprocessed for
|
||||
@@ -164,12 +152,13 @@ def build_inference_frame(
|
||||
tensor-based format suitable for passing to a policy model.
|
||||
|
||||
Args:
|
||||
observation (`dict`): The raw observation dictionary from the robot/environment.
|
||||
device (`device`): The torch device to move the resulting tensors to.
|
||||
ds_features (`dict`): The dataset's feature definitions, used to extract the relevant keys from
|
||||
`observation`.
|
||||
task (`str | None`, *optional*): The task string to attach to the observation, if any.
|
||||
robot_type (`str | None`, *optional*): The robot type string to attach to the observation, if any.
|
||||
observation: The raw observation dictionary, which may contain
|
||||
superfluous keys.
|
||||
device: The target PyTorch device for the final tensors.
|
||||
ds_features: A configuration dictionary that specifies which features
|
||||
to extract from the raw observation.
|
||||
task: An optional string identifier for the current task.
|
||||
robot_type: An optional string identifier for the robot being used.
|
||||
|
||||
Returns:
|
||||
A dictionary of preprocessed tensors ready for model inference.
|
||||
@@ -191,8 +180,10 @@ def make_robot_action(action_tensor: PolicyAction, ds_features: dict[str, dict])
|
||||
action tensor is mapped to a named motor or actuator command.
|
||||
|
||||
Args:
|
||||
action_tensor (`Tensor`): The policy's raw output action tensor.
|
||||
ds_features (`dict`): The dataset's feature definitions, used to name each action dimension.
|
||||
action_tensor: A PyTorch tensor representing the policy's action,
|
||||
typically with a batch dimension (e.g., shape [1, action_dim]).
|
||||
ds_features: A configuration dictionary containing metadata, including
|
||||
the names corresponding to each index of the action tensor.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping action names (e.g., "joint_1_motor") to their
|
||||
@@ -214,7 +205,9 @@ def raise_feature_mismatch_error(
|
||||
provided_features: set[str],
|
||||
expected_features: set[str],
|
||||
) -> None:
|
||||
"""Raises a standardized ValueError for feature mismatches between dataset/environment and policy config."""
|
||||
"""
|
||||
Raises a standardized ValueError for feature mismatches between dataset/environment and policy config.
|
||||
"""
|
||||
missing = expected_features - provided_features
|
||||
extra = provided_features - expected_features
|
||||
# TODO (jadechoghari): provide a dynamic rename map suggestion to the user.
|
||||
@@ -234,7 +227,8 @@ def validate_visual_features_consistency(
|
||||
cfg: PreTrainedConfig,
|
||||
features: dict[str, PolicyFeature],
|
||||
) -> None:
|
||||
"""Validates visual feature consistency between a policy config and provided dataset/environment features.
|
||||
"""
|
||||
Validates visual feature consistency between a policy config and provided dataset/environment features.
|
||||
|
||||
Validation passes if EITHER:
|
||||
- Policy's expected visuals are a subset of dataset (policy uses some cameras, dataset has more)
|
||||
|
||||
@@ -27,184 +27,6 @@ from lerobot.utils.constants import OBS_STATE
|
||||
@PreTrainedConfig.register_subclass("vla_jepa")
|
||||
@dataclass
|
||||
class VLAJEPAConfig(PreTrainedConfig):
|
||||
"""Configuration class for the VLA-JEPA policy.
|
||||
|
||||
VLA-JEPA combines a Qwen3-VL vision-language backbone, a flow-matching (DiT) action head, and an
|
||||
optional V-JEPA2 world model trained to predict future video-frame embeddings from the backbone's
|
||||
action tokens. The world model is an auxiliary training loss only; it is not used at inference.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy. Unused by this policy: the
|
||||
actual observation window is `num_video_frames`, via `observation_delta_indices`.
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the
|
||||
policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 7):
|
||||
The size of the action prediction chunk, in units of environment steps.
|
||||
n_action_steps (`int`, *optional*, defaults to 7):
|
||||
The number of action steps to run in the environment for one invocation of the policy. Must
|
||||
be no greater than `chunk_size`.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to identity normalization for visual features, mean/std for state, and min/max
|
||||
for action features.
|
||||
qwen_model_name (`str`, *optional*, defaults to `"Qwen/Qwen3-VL-2B-Instruct"`):
|
||||
Name or path of the pretrained Qwen3-VL vision-language backbone.
|
||||
jepa_encoder_name (`str`, *optional*, defaults to `"facebook/vjepa2-vitl-fpc64-256"`):
|
||||
Name or path of the pretrained V-JEPA2 encoder used as the world model's (frozen) video
|
||||
target encoder.
|
||||
freeze_qwen (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the Qwen3-VL backbone's parameters during training. Enabling this also
|
||||
disables `enable_world_model`, since no gradient would otherwise flow into it.
|
||||
enable_world_model (`bool`, *optional*, defaults to `True`):
|
||||
Whether to build and train the V-JEPA world-model auxiliary loss and its encoder/predictor
|
||||
modules. Forced to `False` when `freeze_qwen` is `True`.
|
||||
reinit_modules (`list[str] | None`, *optional*):
|
||||
Key prefixes allowed to have shape mismatches when loading pretrained weights, for
|
||||
cross-embodiment transfer to a robot with a different action or state dimensionality (e.g.
|
||||
`["model.action_model.action_encoder", "model.action_model.state_encoder"]`). Mismatched
|
||||
tensors under these prefixes are randomly re-initialized instead of raising; any other
|
||||
mismatch still raises.
|
||||
tokenizer_padding_side (`str`, *optional*, defaults to `"left"`):
|
||||
Padding side used by the Qwen3-VL tokenizer.
|
||||
prompt_template (`str`, *optional*, defaults to `"Your task is {instruction}. Infer the temporal dynamics from frames {actions} and produce the corresponding policy actions {e_actions}."`):
|
||||
Template used to build the language prompt fed to Qwen3-VL, formatted with the task
|
||||
instruction and the action/embodied-action token placeholders.
|
||||
special_action_token (`str`, *optional*, defaults to `"<|action_{}|>"`):
|
||||
Format string for the per-timestep action token(s) added to the tokenizer's vocabulary.
|
||||
embodied_action_token (`str`, *optional*, defaults to `"<|embodied_action|>"`):
|
||||
Special token added to the tokenizer's vocabulary, whose hidden states condition the action
|
||||
head.
|
||||
action_dim (`int`, *optional*, defaults to 7):
|
||||
Dimensionality of the action vector. Overwritten from the dataset's action feature shape in
|
||||
`validate_features`.
|
||||
state_dim (`int`, *optional*, defaults to 8):
|
||||
Dimensionality of the robot state vector. Overwritten from the dataset's state feature shape
|
||||
in `validate_features`, when a state feature is present.
|
||||
num_action_tokens_per_timestep (`int`, *optional*, defaults to 8):
|
||||
Number of action tokens allocated per prompted timestep in the Qwen3-VL prompt.
|
||||
num_embodied_action_tokens_per_instruction (`int`, *optional*, defaults to 32):
|
||||
Number of embodied-action tokens allocated per instruction in the Qwen3-VL prompt; also sets
|
||||
the action head's future-token embedding count.
|
||||
num_inference_timesteps (`int`, *optional*, defaults to 4):
|
||||
Number of integration steps used by the flow-matching action head at inference time.
|
||||
action_hidden_size (`int`, *optional*, defaults to 1024):
|
||||
Hidden dimension of the action head's DiT output projection.
|
||||
action_model_type (`str`, *optional*, defaults to `"DiT-B"`):
|
||||
Named DiT preset (`"DiT-B"`, `"DiT-L"`, or `"DiT-test"`) providing default attention head
|
||||
count and head dimension for the action head, unless overridden by `action_num_heads` and
|
||||
`action_attention_head_dim`.
|
||||
action_num_layers (`int`, *optional*, defaults to 16):
|
||||
Number of transformer blocks in the action head's DiT.
|
||||
action_num_heads (`int | None`, *optional*):
|
||||
Number of attention heads for the action head's DiT. `None` uses the `action_model_type`
|
||||
preset's value.
|
||||
action_attention_head_dim (`int | None`, *optional*):
|
||||
Per-head attention dimension for the action head's DiT. `None` uses the `action_model_type`
|
||||
preset's value.
|
||||
action_dropout (`float`, *optional*, defaults to 0.2):
|
||||
Dropout used in the action head's DiT transformer blocks.
|
||||
action_num_timestep_buckets (`int`, *optional*, defaults to 1000):
|
||||
Number of discrete buckets the continuous flow-matching timestep is quantized into before
|
||||
being embedded.
|
||||
action_noise_beta_alpha (`float`, *optional*, defaults to 1.5):
|
||||
Alpha parameter of the Beta distribution used to sample the flow-matching timestep during
|
||||
training.
|
||||
action_noise_beta_beta (`float`, *optional*, defaults to 1.0):
|
||||
Beta parameter of the Beta distribution used to sample the flow-matching timestep during
|
||||
training.
|
||||
action_noise_s (`float`, *optional*, defaults to 0.999):
|
||||
Scale used to rescale the Beta-sampled value into a flow-matching timestep, as
|
||||
`(action_noise_s - sample) / action_noise_s`.
|
||||
num_target_vision_tokens (`int`, *optional*, defaults to 32):
|
||||
Reserved configuration field; not currently referenced by the model implementation.
|
||||
action_max_seq_len (`int`, *optional*, defaults to 1024):
|
||||
Reserved configuration field; not currently referenced by the model implementation.
|
||||
num_video_frames (`int`, *optional*, defaults to 8):
|
||||
Total number of video frames loaded per sample for the world model.
|
||||
predictor_depth (`int`, *optional*, defaults to 12):
|
||||
Number of transformer blocks in the world model's video predictor.
|
||||
predictor_num_heads (`int`, *optional*, defaults to 8):
|
||||
Number of attention heads in the world model's video predictor.
|
||||
predictor_mlp_ratio (`float`, *optional*, defaults to 4.0):
|
||||
Feed-forward expansion ratio in the world model's video predictor.
|
||||
predictor_dropout (`float`, *optional*, defaults to 0.0):
|
||||
Reserved configuration field; not currently referenced by the model implementation.
|
||||
world_model_loss_weight (`float`, *optional*, defaults to 0.1):
|
||||
Weight applied to the world-model's video-prediction loss before adding it to the action
|
||||
loss.
|
||||
jepa_tubelet_size (`int`, *optional*, defaults to 2):
|
||||
Number of camera views the world model consumes (video tensors are padded or trimmed to this
|
||||
many views), and the fallback tubelet size used to size action-token prompt placeholders
|
||||
when the world model is disabled. Should match the JEPA encoder's actual tubelet size (e.g.
|
||||
2 for `vjepa2-vitl-fpc64-256`).
|
||||
repeated_diffusion_steps (`int`, *optional*, defaults to 8):
|
||||
Number of independent noise draws per batch item used to repeat the flow-matching loss
|
||||
computation (CogACT-style).
|
||||
resize_images_to (`tuple[int, int] | None`, *optional*):
|
||||
Target `(height, width)` to resize input images to before inference. `None` (the default)
|
||||
keeps the original resolution. Not applied during training.
|
||||
binarize_gripper_action (`bool`, *optional*, defaults to `True`):
|
||||
Whether to binarize the gripper action dimension after unnormalization, in the
|
||||
post-processing pipeline built by `make_vla_jepa_pre_post_processors`.
|
||||
pre_snap_gripper_action (`bool`, *optional*, defaults to `True`):
|
||||
Whether to snap the gripper action dimension to `{0, 1}` before unnormalization, in the
|
||||
post-processing pipeline built by `make_vla_jepa_pre_post_processors`.
|
||||
clip_normalized_actions (`bool`, *optional*, defaults to `True`):
|
||||
Whether to clip normalized actions to `[-1, 1]` before unnormalization, in the
|
||||
post-processing pipeline built by `make_vla_jepa_pre_post_processors`.
|
||||
gripper_dim (`int`, *optional*, defaults to 6):
|
||||
Index of the gripper dimension within the action vector, used by the gripper pre/post
|
||||
-processing steps.
|
||||
gripper_threshold (`float`, *optional*, defaults to 0.5):
|
||||
Threshold used by the gripper pre/post-processing steps to binarize the gripper action.
|
||||
torch_dtype (`str`, *optional*, defaults to `"bfloat16"`):
|
||||
Torch dtype (`"float32"`, `"float16"`, or `"bfloat16"`) used to load the Qwen3-VL backbone
|
||||
and (when enabled) the V-JEPA world-model encoder.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Learning rate for the AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`):
|
||||
Adam beta coefficients for the AdamW optimizer preset.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Epsilon for the AdamW optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-10):
|
||||
Weight decay for the AdamW optimizer preset.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 10.0):
|
||||
Gradient-clipping norm for the AdamW optimizer preset.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler preset.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the cosine-decay-with-warmup learning rate scheduler preset.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Final learning rate at the end of decay, for the cosine-decay-with-warmup scheduler preset.
|
||||
"""
|
||||
|
||||
n_obs_steps: int = 1
|
||||
chunk_size: int = 7
|
||||
n_action_steps: int = 7
|
||||
@@ -221,6 +43,11 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
jepa_encoder_name: str = "facebook/vjepa2-vitl-fpc64-256"
|
||||
freeze_qwen: bool = False
|
||||
enable_world_model: bool = True
|
||||
# Enables cross-embodiment transfer: when fine-tuning a pretrained model on a robot with a
|
||||
# different action or state dimensionality, the input/output projection layers must be
|
||||
# re-initialised from scratch while the rest of the network keeps its pretrained weights.
|
||||
# List the key prefixes that are allowed to have shape mismatches; anything else raises an error.
|
||||
# e.g. ["model.action_model.action_encoder", "model.action_model.state_encoder"]
|
||||
reinit_modules: list[str] | None = None
|
||||
|
||||
tokenizer_padding_side: str = "left"
|
||||
@@ -248,14 +75,15 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
num_target_vision_tokens: int = 32
|
||||
action_max_seq_len: int = 1024
|
||||
|
||||
# total video frames loaded per sample
|
||||
num_video_frames: int = 8
|
||||
predictor_depth: int = 12
|
||||
predictor_num_heads: int = 8
|
||||
predictor_mlp_ratio: float = 4.0
|
||||
predictor_dropout: float = 0.0
|
||||
world_model_loss_weight: float = 0.1
|
||||
jepa_tubelet_size: int = 2
|
||||
repeated_diffusion_steps: int = 8
|
||||
jepa_tubelet_size: int = 2 # must match the encoder (e.g. 2 for vjepa2-vitl-fpc64-256)
|
||||
repeated_diffusion_steps: int = 8 # independent noise draws per batch item (CogACT-style)
|
||||
|
||||
resize_images_to: tuple[int, int] | None = None
|
||||
binarize_gripper_action: bool = True
|
||||
@@ -275,7 +103,6 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
scheduler_decay_lr: float = 2.5e-6
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the JEPA world-model and action-head configuration."""
|
||||
super().__post_init__()
|
||||
if self.freeze_qwen and self.enable_world_model:
|
||||
# freezing qwen backbone makes world model training irrelevant since no grad flows
|
||||
@@ -289,7 +116,6 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if not self.image_features:
|
||||
raise ValueError("VLAJEPA requires at least one visual input feature.")
|
||||
if self.action_feature is None:
|
||||
@@ -306,7 +132,6 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
self.input_features[OBS_STATE] = PolicyFeature(type=FeatureType.STATE, shape=shape)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -316,7 +141,6 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> CosineDecayWithWarmupSchedulerConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -326,17 +150,14 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
# load video_horizon frames starting from current timestep: [t, t+1, ..., t+video_horizon-1]
|
||||
# matches original repo's observation_indices=list(range(video_horizon))
|
||||
return list(range(self.num_video_frames))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -62,7 +62,8 @@ def _get_autocast_context(device_type: str, dtype: torch.dtype = torch.bfloat16)
|
||||
|
||||
|
||||
class VLAJEPAModel(nn.Module):
|
||||
"""Native VLA-JEPA model following the original starVLA VLA_JEPA.py.
|
||||
"""
|
||||
Native VLA-JEPA model following the original starVLA VLA_JEPA.py.
|
||||
|
||||
Components:
|
||||
- Qwen3-VL: vision-language backbone for fused embeddings
|
||||
@@ -329,7 +330,8 @@ class VLAJEPAModel(nn.Module):
|
||||
|
||||
|
||||
class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
"""LeRobot adapter for VLA-JEPA.
|
||||
"""
|
||||
LeRobot adapter for VLA-JEPA.
|
||||
|
||||
Converts LeRobot's standard batch format (dict[str, Tensor]) to the batched tensors
|
||||
the native model expects (keeping everything on-device), calls the native model, and
|
||||
@@ -340,15 +342,6 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
name = "vla_jepa"
|
||||
|
||||
def __init__(self, config: VLAJEPAConfig, **kwargs) -> None:
|
||||
"""Build the native VLA-JEPA model: Qwen3-VL backbone, flow-matching action head, and optional
|
||||
V-JEPA world model.
|
||||
|
||||
Args:
|
||||
config (VLAJEPAConfig): The policy configuration.
|
||||
kwargs: Forwarded to the base class. If `dataset_meta` is present, it is used to override
|
||||
`config.state_dim`/`config.action_dim` from the actual dataset's feature shapes, in case
|
||||
`validate_features` read stale dimensions from a pretrained config.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
if dataset_meta := kwargs.get("dataset_meta"):
|
||||
@@ -365,9 +358,6 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Reinitializes the action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
self._queues = {ACTION: deque(maxlen=self.config.n_action_steps)}
|
||||
|
||||
# ---- Format Conversion: LeRobot → Native ----
|
||||
@@ -435,18 +425,7 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
# ---- LeRobot Policy Interface ----
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Converts the batch to the native model's inputs, then runs the flow-matching action loss and,
|
||||
when `enable_world_model` is set, the V-JEPA world-model loss, returning their sum.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation/action tensors.
|
||||
|
||||
Returns:
|
||||
tuple[Tensor, dict]: The total loss (`action_loss` plus the weighted `wm_loss`) and a dict of
|
||||
the individual loss terms for logging.
|
||||
"""
|
||||
"""LeRobot train forward: convert → native forward → aggregate losses."""
|
||||
native_output = self.model.forward(**self._prepare_model_inputs(batch, training=True))
|
||||
|
||||
ref = next(iter(native_output.values()))
|
||||
@@ -457,27 +436,11 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
return total_loss, logs
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Returns all of the model's parameters directly (an iterator), rather than a grouped dict.
|
||||
"""
|
||||
return self.model.parameters()
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Converts the batch to the native model's inputs and runs the flow-matching action head's
|
||||
`predict_action` to produce the full predicted chunk.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation tensors.
|
||||
noise (Tensor | None, *optional*): Accepted for interface compatibility with
|
||||
`ActionSelectKwargs`; currently unused.
|
||||
|
||||
Returns:
|
||||
Tensor: The predicted action chunk.
|
||||
"""
|
||||
"""LeRobot inference: convert → native predict → return as Tensor."""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
|
||||
@@ -487,11 +450,7 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`: the queue is refilled with the first
|
||||
`n_action_steps` predicted actions whenever it runs empty.
|
||||
"""
|
||||
"""LeRobot select_action with action queue caching."""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
if len(self._queues[ACTION]) == 0:
|
||||
@@ -505,7 +464,6 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
pretrained_name_or_path: str | Path,
|
||||
**kwargs,
|
||||
):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`]."""
|
||||
return super().from_pretrained(pretrained_name_or_path, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -107,26 +107,6 @@ def make_vla_jepa_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Construct the pre-processor and post-processor pipelines for the VLA-JEPA policy.
|
||||
|
||||
The pre-processing pipeline renames observation features, adds a batch dimension, moves data to the
|
||||
configured device, and normalizes input/output features.
|
||||
|
||||
The post-processing pipeline unnormalizes both input and output features (unlike the default policy
|
||||
unnormalizer, which only covers output features), and optionally clips normalized actions and
|
||||
snaps/binarizes a gripper action dimension around the unnormalization step, depending on
|
||||
`config.clip_normalized_actions`, `config.pre_snap_gripper_action`, and
|
||||
`config.binarize_gripper_action`.
|
||||
|
||||
Args:
|
||||
config (VLAJEPAConfig): The policy configuration.
|
||||
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): Statistics used for
|
||||
normalizing and unnormalizing features. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]: The configured pre-processor and
|
||||
post-processor pipelines.
|
||||
"""
|
||||
features = {**config.input_features, **config.output_features}
|
||||
steps = make_default_policy_processor_steps(config, dataset_stats)
|
||||
input_steps = [
|
||||
|
||||
@@ -40,108 +40,43 @@ class VQBeTConfig(PreTrainedConfig):
|
||||
- "action" is required as an output key.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 5):
|
||||
Number of environment steps of observation to pass to the policy (the current step and
|
||||
additional steps going back).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy
|
||||
is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
n_action_pred_token (`int`, *optional*, defaults to 3):
|
||||
Total number of current token and future tokens that VQ-BeT predicts.
|
||||
action_chunk_size (`int`, *optional*, defaults to 5):
|
||||
Action chunk size of each action prediction token.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to identity normalization for visual features and min/max normalization for
|
||||
state and action features.
|
||||
vision_backbone (`str`, *optional*, defaults to `"resnet18"`):
|
||||
Name of the torchvision resnet backbone to use for encoding images.
|
||||
crop_shape (`tuple[int, int] | None`, *optional*, defaults to `(84, 84)`):
|
||||
(H, W) shape to crop images to as a preprocessing step for the vision backbone. Must fit
|
||||
within the image size. `None` means no cropping is done.
|
||||
crop_is_random (`bool`, *optional*, defaults to `True`):
|
||||
Whether the crop should be random at training time (it's always a center crop in eval mode).
|
||||
pretrained_backbone_weights (`str | None`, *optional*, defaults to `"ResNet18_Weights.IMAGENET1K_V1"`):
|
||||
Pretrained weights from torchvision to initialize the backbone. `None` means no pretrained
|
||||
weights.
|
||||
use_group_norm (`bool`, *optional*, defaults to `False`):
|
||||
Whether to replace batch normalization with group normalization in the backbone. The group
|
||||
sizes are set to be about 16 (`feature_dim // 16`).
|
||||
spatial_softmax_num_keypoints (`int`, *optional*, defaults to 32):
|
||||
Number of keypoints for SpatialSoftmax.
|
||||
n_vqvae_training_steps (`int`, *optional*, defaults to 20000):
|
||||
Number of optimization steps for training the Residual VQ.
|
||||
vqvae_n_embed (`int`, *optional*, defaults to 16):
|
||||
Number of embedding vectors in the RVQ dictionary (each layer).
|
||||
vqvae_embedding_dim (`int`, *optional*, defaults to 256):
|
||||
Dimension of each embedding vector in the RVQ dictionary.
|
||||
vqvae_enc_hidden_dim (`int`, *optional*, defaults to 128):
|
||||
Size of hidden dimensions of the encoder/decoder part of the Residual VQ-VAE.
|
||||
gpt_block_size (`int`, *optional*, defaults to 500):
|
||||
Max block size of minGPT (should be larger than the number of input tokens).
|
||||
gpt_input_dim (`int`, *optional*, defaults to 512):
|
||||
Size of input of GPT. This is also used as the dimension of observation features.
|
||||
gpt_output_dim (`int`, *optional*, defaults to 512):
|
||||
Size of output dimension of GPT. This is also used as an input dimension of the offset / bin
|
||||
prediction headers.
|
||||
gpt_n_layer (`int`, *optional*, defaults to 8):
|
||||
Number of layers of GPT.
|
||||
gpt_n_head (`int`, *optional*, defaults to 8):
|
||||
Number of heads of GPT.
|
||||
gpt_hidden_dim (`int`, *optional*, defaults to 512):
|
||||
Size of hidden dimensions of GPT.
|
||||
dropout (`float`, *optional*, defaults to 0.1):
|
||||
Dropout rate for GPT.
|
||||
offset_loss_weight (`float`, *optional*, defaults to 10000.0):
|
||||
A constant that is multiplied to the offset loss.
|
||||
primary_code_loss_weight (`float`, *optional*, defaults to 5.0):
|
||||
A constant that is multiplied to the primary code prediction loss.
|
||||
secondary_code_loss_weight (`float`, *optional*, defaults to 0.5):
|
||||
A constant that is multiplied to the secondary code prediction loss.
|
||||
bet_softmax_temperature (`float`, *optional*, defaults to 0.1):
|
||||
Sampling temperature of code for rollout with VQ-BeT.
|
||||
sequentially_select (`bool`, *optional*, defaults to `False`):
|
||||
Whether to select the primary / secondary code sequentially (pick the primary code, then
|
||||
select the secondary code), or at the same time.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Learning rate for the Adam optimizer preset (GPT and other non-VQ-VAE parameters).
|
||||
optimizer_betas (`tuple`, *optional*, defaults to `(0.95, 0.999)`):
|
||||
Adam optimizer's beta coefficients.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Adam optimizer's epsilon for numerical stability.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 1e-06):
|
||||
Weight decay for the Adam optimizer preset.
|
||||
optimizer_vqvae_lr (`float`, *optional*, defaults to 0.001):
|
||||
Learning rate for the VQ-VAE's own Adam optimizer preset.
|
||||
optimizer_vqvae_weight_decay (`float`, *optional*, defaults to 0.0001):
|
||||
Weight decay for the VQ-VAE's own Adam optimizer preset.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 500):
|
||||
Number of warmup steps for the LR scheduler preset.
|
||||
n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the
|
||||
current step and additional steps going back).
|
||||
n_action_pred_token: Total number of current token and future tokens that VQ-BeT predicts.
|
||||
action_chunk_size: Action chunk size of each action prediction token.
|
||||
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents
|
||||
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents
|
||||
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
||||
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
||||
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
||||
vision_backbone: Name of the torchvision resnet backbone to use for encoding images.
|
||||
crop_shape: (H, W) shape to crop images to as a preprocessing step for the vision backbone. Must fit
|
||||
within the image size. If None, no cropping is done.
|
||||
crop_is_random: Whether the crop should be random at training time (it's always a center crop in eval
|
||||
mode).
|
||||
pretrained_backbone_weights: Pretrained weights from torchvision to initialize the backbone.
|
||||
`None` means no pretrained weights.
|
||||
use_group_norm: Whether to replace batch normalization with group normalization in the backbone.
|
||||
The group sizes are set to be about 16 (to be precise, feature_dim // 16).
|
||||
spatial_softmax_num_keypoints: Number of keypoints for SpatialSoftmax.
|
||||
n_vqvae_training_steps: Number of optimization steps for training Residual VQ.
|
||||
vqvae_n_embed: Number of embedding vectors in the RVQ dictionary (each layer).
|
||||
vqvae_embedding_dim: Dimension of each embedding vector in the RVQ dictionary.
|
||||
vqvae_enc_hidden_dim: Size of hidden dimensions of Encoder / Decoder part of Residaul VQ-VAE
|
||||
gpt_block_size: Max block size of minGPT (should be larger than the number of input tokens)
|
||||
gpt_input_dim: Size of output input of GPT. This is also used as the dimension of observation features.
|
||||
gpt_output_dim: Size of output dimension of GPT. This is also used as a input dimension of offset / bin prediction headers.
|
||||
gpt_n_layer: Number of layers of GPT
|
||||
gpt_n_head: Number of headers of GPT
|
||||
gpt_hidden_dim: Size of hidden dimensions of GPT
|
||||
dropout: Dropout rate for GPT
|
||||
offset_loss_weight: A constant that is multiplied to the offset loss
|
||||
primary_code_loss_weight: A constant that is multiplied to the primary code prediction loss
|
||||
secondary_code_loss_weight: A constant that is multiplied to the secondary code prediction loss
|
||||
bet_softmax_temperature: Sampling temperature of code for rollout with VQ-BeT
|
||||
sequentially_select: Whether select code of primary / secondary as sequentially (pick primary code,
|
||||
and then select secodnary code), or at the same time.
|
||||
"""
|
||||
|
||||
# Inputs / output structure.
|
||||
@@ -194,16 +129,15 @@ class VQBeTConfig(PreTrainedConfig):
|
||||
scheduler_warmup_steps: int = 500
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the VQ-VAE and action-chunking configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
"""Input validation (not exhaustive)."""
|
||||
if not self.vision_backbone.startswith("resnet"):
|
||||
raise ValueError(
|
||||
f"`vision_backbone` must be one of the ResNet variants. Got {self.vision_backbone}."
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_optimizer_preset`]."""
|
||||
return AdamConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -212,14 +146,12 @@ class VQBeTConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> VQBeTSchedulerConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return VQBeTSchedulerConfig(
|
||||
num_warmup_steps=self.scheduler_warmup_steps,
|
||||
num_vqvae_training_steps=self.n_vqvae_training_steps,
|
||||
)
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
# Note: this check was previously performed inside VQBeTRgbEncoder in the form of
|
||||
# assert len(image_keys) == 1
|
||||
if not len(self.image_features) == 1:
|
||||
@@ -244,15 +176,12 @@ class VQBeTConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, 1))
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(1 - self.n_obs_steps, self.n_action_pred_token + self.action_chunk_size - 1))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -38,7 +38,9 @@ from .vqbet_utils import GPT, ResidualVQ
|
||||
|
||||
|
||||
class VQBeTPolicy(PreTrainedPolicy):
|
||||
"""VQ-BeT Policy as per "Behavior Generation with Latent Actions" """
|
||||
"""
|
||||
VQ-BeT Policy as per "Behavior Generation with Latent Actions"
|
||||
"""
|
||||
|
||||
config_class = VQBeTConfig
|
||||
name = "vqbet"
|
||||
@@ -48,11 +50,12 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
config: VQBeTConfig | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Build the VQ-BeT model from `config`.
|
||||
|
||||
"""
|
||||
Args:
|
||||
config (`VQBeTConfig | None`):
|
||||
Policy configuration.
|
||||
config: Policy configuration class instance or None, in which case the default instantiation of
|
||||
the configuration class is used.
|
||||
dataset_stats: Dataset statistics to be used for normalization. If not passed here, it is expected
|
||||
that they will be passed with a call to `load_state_dict` before the policy is used.
|
||||
"""
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -63,12 +66,6 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
|
||||
Splits parameters into three groups: the RVQ-VAE parameters (trained at
|
||||
`optimizer_vqvae_lr`/`optimizer_vqvae_weight_decay`), weight-decayed parameters, and
|
||||
non-weight-decayed parameters.
|
||||
"""
|
||||
vqvae_params = (
|
||||
list(self.vqbet.action_head.vqvae_model.encoder.parameters())
|
||||
+ list(self.vqbet.action_head.vqvae_model.decoder.parameters())
|
||||
@@ -109,9 +106,9 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
]
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
|
||||
|
||||
Clears the observation and action queues populated during rollout of the policy.
|
||||
"""
|
||||
Clear observation and action queues. Should be called on `env.reset()`
|
||||
queues are populated during rollout of the policy, they contain the n latest observations and actions
|
||||
"""
|
||||
self._queues = {
|
||||
OBS_IMAGES: deque(maxlen=self.config.n_obs_steps),
|
||||
@@ -121,16 +118,17 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]."""
|
||||
batch = {k: torch.stack(list(self._queues[k]), dim=1) for k in batch if k in self._queues}
|
||||
actions = self.vqbet(batch, rollout=True)[:, : self.config.action_chunk_size]
|
||||
return actions
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
"""Select a single action given environment observations.
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`, refilling it once it runs dry.
|
||||
This method wraps `select_actions` in order to return one action at a time for execution in the
|
||||
environment. It works by managing the actions in a queue and only calling `select_actions` when the
|
||||
queue is empty.
|
||||
"""
|
||||
# NOTE: for offline evaluation, we have action in the batch, so we need to pop it out
|
||||
if ACTION in batch:
|
||||
@@ -159,11 +157,7 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
return action
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Until the Residual VQ-VAE is discretized, this trains the VQ-VAE (returning its reconstruction
|
||||
loss and codebook usage stats); afterwards it trains VQ-BeT's GPT and prediction heads instead.
|
||||
"""
|
||||
"""Run the batch through the model and compute the loss for training or validation."""
|
||||
batch = dict(batch) # shallow copy so that adding a key doesn't modify the original
|
||||
batch[OBS_IMAGES] = torch.stack([batch[key] for key in self.config.image_features], dim=-4)
|
||||
# VQ-BeT discretizes action using VQ-VAE before training BeT (please refer to section 3.2 in the VQ-BeT paper https://huggingface.co/papers/2403.03181)
|
||||
@@ -187,7 +181,8 @@ class VQBeTPolicy(PreTrainedPolicy):
|
||||
|
||||
|
||||
class SpatialSoftmax(nn.Module):
|
||||
"""Spatial Soft Argmax operation described in "Deep Spatial Autoencoders for Visuomotor Learning" by Finn et al.
|
||||
"""
|
||||
Spatial Soft Argmax operation described in "Deep Spatial Autoencoders for Visuomotor Learning" by Finn et al.
|
||||
(https://huggingface.co/papers/1509.06113). A minimal port of the robomimic implementation.
|
||||
|
||||
At a high level, this takes 2D feature maps (from a convnet/ViT) and returns the "center of mass"
|
||||
@@ -209,9 +204,10 @@ class SpatialSoftmax(nn.Module):
|
||||
"""
|
||||
|
||||
def __init__(self, input_shape, num_kp=None):
|
||||
"""Args:
|
||||
input_shape (list): (C, H, W) input feature map shape.
|
||||
num_kp (int): number of keypoints in output. If None, output will have the same number of channels as input.
|
||||
"""
|
||||
Args:
|
||||
input_shape (list): (C, H, W) input feature map shape.
|
||||
num_kp (int): number of keypoints in output. If None, output will have the same number of channels as input.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
@@ -234,9 +230,9 @@ class SpatialSoftmax(nn.Module):
|
||||
self.register_buffer("pos_grid", torch.cat([pos_x, pos_y], dim=1))
|
||||
|
||||
def forward(self, features: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
features: (B, C, H, W) input feature maps.
|
||||
|
||||
Returns:
|
||||
(B, K, 2) image-space coordinates of keypoints.
|
||||
"""
|
||||
@@ -409,7 +405,8 @@ class VQBeTModel(nn.Module):
|
||||
|
||||
class VQBeTHead(nn.Module):
|
||||
def __init__(self, config: VQBeTConfig):
|
||||
"""VQBeTHead takes output of GPT layers, and pass the feature through bin prediction head (`self.map_to_cbet_preds_bin`), and offset prediction head (`self.map_to_cbet_preds_offset`)
|
||||
"""
|
||||
VQBeTHead takes output of GPT layers, and pass the feature through bin prediction head (`self.map_to_cbet_preds_bin`), and offset prediction head (`self.map_to_cbet_preds_offset`)
|
||||
|
||||
self.map_to_cbet_preds_bin: outputs probability of each code (for each layer).
|
||||
The input dimension of `self.map_to_cbet_preds_bin` is same with the output of GPT,
|
||||
@@ -420,6 +417,7 @@ class VQBeTHead(nn.Module):
|
||||
The input dimension of ` self.map_to_cbet_preds_offset` is same with the output of GPT,
|
||||
and the output dimension of ` self.map_to_cbet_preds_offset` is `self.vqvae_model.vqvae_num_layers (=fixed as 2) * self.config.vqvae_n_embed * config.action_chunk_size * config.action_feature.shape[0]`.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.config = config
|
||||
# init vqvae
|
||||
@@ -575,7 +573,8 @@ class VQBeTHead(nn.Module):
|
||||
}
|
||||
|
||||
def loss_fn(self, pred, target, **kwargs):
|
||||
"""For given ground truth action values (target), and prediction (pred) this function calculates the overall loss.
|
||||
"""
|
||||
for given ground truth action values (target), and prediction (pred) this function calculates the overall loss.
|
||||
|
||||
predicted_action: predicted action chunk (offset + decoded centroids)
|
||||
sampled_centers: sampled centroids (code of RVQ)
|
||||
@@ -704,9 +703,9 @@ class VQBeTRgbEncoder(nn.Module):
|
||||
self.relu = nn.ReLU()
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
x: (B, C, H, W) image tensor with pixel values in [0, 1].
|
||||
|
||||
Returns:
|
||||
(B, D) image feature.
|
||||
"""
|
||||
@@ -727,11 +726,11 @@ class VQBeTRgbEncoder(nn.Module):
|
||||
def _replace_submodules(
|
||||
root_module: nn.Module, predicate: Callable[[nn.Module], bool], func: Callable[[nn.Module], nn.Module]
|
||||
) -> nn.Module:
|
||||
"""Args:
|
||||
"""
|
||||
Args:
|
||||
root_module: The module for which the submodules need to be replaced
|
||||
predicate: Takes a module as an argument and must return True if the that module is to be replaced.
|
||||
func: Takes a module as an argument and returns a new module to replace it with.
|
||||
|
||||
Returns:
|
||||
The root module with its submodules replaced.
|
||||
"""
|
||||
@@ -762,13 +761,15 @@ class VqVae(nn.Module):
|
||||
self,
|
||||
config: VQBeTConfig,
|
||||
):
|
||||
"""VQ-VAE is composed of three parts: encoder, vq_layer, and decoder.
|
||||
"""
|
||||
VQ-VAE is composed of three parts: encoder, vq_layer, and decoder.
|
||||
Encoder and decoder are MLPs consisting of an input, output layer, and hidden layer, respectively.
|
||||
The vq_layer uses residual VQs.
|
||||
|
||||
This class contains functions for training the encoder and decoder along with the residual VQ layer (for training phase 1),
|
||||
as well as functions to help BeT training part in training phase 2.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.config = config
|
||||
# 'discretized' indicates whether the Residual VQ part is trained or not. (After finishing the training, we set discretized=True)
|
||||
@@ -860,7 +861,9 @@ class VqVae(nn.Module):
|
||||
|
||||
|
||||
class FocalLoss(nn.Module):
|
||||
"""From https://github.com/notmahi/miniBET/blob/main/behavior_transformer/bet.py"""
|
||||
"""
|
||||
From https://github.com/notmahi/miniBET/blob/main/behavior_transformer/bet.py
|
||||
"""
|
||||
|
||||
def __init__(self, gamma: float = 0, size_average: bool = True):
|
||||
super().__init__()
|
||||
|
||||
@@ -35,7 +35,8 @@ def make_vqbet_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the VQ-BeT policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the VQ-BeT policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features, allowing customization to match pretrained configurations.
|
||||
@@ -48,8 +49,8 @@ def make_vqbet_pre_post_processors(
|
||||
2. Unnormalizing the output features to their original scale.
|
||||
|
||||
Args:
|
||||
config (`VQBeTConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the VQ-BeT policy.
|
||||
dataset_stats: A dictionary of statistics for normalization.
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines.
|
||||
|
||||
@@ -22,61 +22,13 @@ from lerobot.utils.constants import ACTION, OBS_STATE
|
||||
@PreTrainedConfig.register_subclass("wall_x")
|
||||
@dataclass
|
||||
class WallXConfig(PreTrainedConfig):
|
||||
"""Configuration class for the Wall-X policy.
|
||||
"""
|
||||
Configuration class for Wall-X policy.
|
||||
|
||||
Wall-X is based on Qwen2.5-VL with action prediction capabilities using flow matching. It supports
|
||||
cross-embodiment robotic control through unified action representations, and multi-modal learning
|
||||
with vision, language, and action data.
|
||||
Wall-X is based on Qwen2.5-VL with action prediction capabilities using flow matching.
|
||||
It supports cross-embodiment robotic control through unified action representations.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1): Number of environment steps of observation to
|
||||
pass to the policy (the current step plus this many additional steps looking back).
|
||||
input_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from input feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, lerobot.configs.types.PolicyFeature] | None`, *optional*): Mapping from output feature name to its `PolicyFeature` (type and shape). Populated automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*): Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. If unset or unavailable, auto-selected on construction.
|
||||
use_amp (`bool`, *optional*, defaults to `False`): Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`): Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`): Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*): Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*): Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*): Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*): License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`pathlib.Path | None`, *optional*): Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*): Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 32): The size of the action prediction chunk
|
||||
(`action_horizon` in Wall-X terminology).
|
||||
n_action_steps (`int`, *optional*, defaults to 32): The number of actions from a predicted
|
||||
chunk that are actually queued for execution. Must not exceed `chunk_size`.
|
||||
max_action_dim (`int`, *optional*, defaults to 20): Maximum action dimension Wall-X supports;
|
||||
shorter actions are zero-padded.
|
||||
max_state_dim (`int`, *optional*, defaults to 20): Maximum proprioceptive-state dimension
|
||||
Wall-X supports; shorter states are zero-padded.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*): Per-feature-type
|
||||
normalization mode; defaults to `IDENTITY` for vision and `MEAN_STD` for state/action.
|
||||
pretrained_name_or_path (`str`, *optional*, defaults to `"x-square-robot/wall-oss-flow"`): Hub id
|
||||
or local path of the pretrained Wall-X model to load.
|
||||
action_tokenizer_path (`str | None`, *optional*, defaults to `"lerobot/fast-action-tokenizer"`): Hub
|
||||
id of the FAST action tokenizer, used only when `prediction_mode="fast"`. Forced to `None` in
|
||||
`__post_init__` when `prediction_mode` is `"diffusion"`.
|
||||
prediction_mode (`str`, *optional*, defaults to `"diffusion"`): Action prediction mode:
|
||||
`"diffusion"` (flow matching) or `"fast"` (discrete FAST tokens).
|
||||
attn_implementation (`str`, *optional*, defaults to `"eager"`): Attention backend for the
|
||||
language/action-token model. Only `"eager"` is currently supported, since Wall-X's
|
||||
bidirectional action-token islands require an explicit attention mask.
|
||||
vision_attn_implementation (`str`, *optional*, defaults to `"auto"`): Attention backend for
|
||||
vision, independent from the text action-token mask: `"auto"` (packed variable-length
|
||||
attention when supported, otherwise per-chunk SDPA), `"sdpa"`, or `"varlen"`.
|
||||
optimizer_lr (`float`, *optional*, defaults to 2e-05): AdamW learning rate.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.95)`): AdamW betas.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08): AdamW epsilon.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.01): AdamW weight decay.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 1.0): Gradient clipping norm.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000): Number of warmup steps for the
|
||||
cosine-decay-with-warmup scheduler.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 100000): Number of decay steps for the
|
||||
scheduler.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 1e-06): Final learning rate at the end of
|
||||
the decay schedule.
|
||||
This config supports multi-modal learning with vision, language, and action data.
|
||||
"""
|
||||
|
||||
# ==================== Input / Output Structure ====================
|
||||
@@ -126,13 +78,6 @@ class WallXConfig(PreTrainedConfig):
|
||||
scheduler_decay_lr: float = 1e-6
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate cross-field constraints and derive `use_fast_tokenizer` from `prediction_mode`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `n_action_steps` exceeds `chunk_size`, if `prediction_mode` is not
|
||||
`"diffusion"` or `"fast"`, if `attn_implementation` is not `"eager"`, or if
|
||||
`vision_attn_implementation` is not one of `"auto"`, `"sdpa"`, or `"varlen"`.
|
||||
"""
|
||||
super().__post_init__()
|
||||
|
||||
# Input validation
|
||||
@@ -206,7 +151,6 @@ class WallXConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
"""Return the AdamW optimizer configuration built from the `optimizer_*` fields."""
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
betas=self.optimizer_betas,
|
||||
@@ -216,7 +160,6 @@ class WallXConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self):
|
||||
"""Return the cosine-decay-with-warmup scheduler configuration built from the `scheduler_*` fields."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -226,15 +169,12 @@ class WallXConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list:
|
||||
"""Return indices for delta observations (None for Wall-X)."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list:
|
||||
"""Return indices for delta actions."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> None:
|
||||
"""Return indices for delta rewards (None for Wall-X)."""
|
||||
return None
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Wall-X: Cross-embodiment robotic control using Qwen2.5-VL with flow matching.
|
||||
"""
|
||||
Wall-X: Cross-embodiment robotic control using Qwen2.5-VL with flow matching.
|
||||
|
||||
[Paper](https://github.com/x2-robot/wall-x)
|
||||
|
||||
@@ -201,7 +202,8 @@ class SinusoidalPosEmb(nn.Module):
|
||||
|
||||
|
||||
class ActionHead(nn.Module):
|
||||
"""Action prediction head with flow matching.
|
||||
"""
|
||||
Action prediction head with flow matching.
|
||||
|
||||
Implements Beta-distributed noise scheduling and temporal embeddings
|
||||
for action sequence prediction.
|
||||
@@ -247,7 +249,8 @@ class ActionHead(nn.Module):
|
||||
return time
|
||||
|
||||
def forward(self, action_chunk, dof_mask=None):
|
||||
"""Process action sequences with noise injection for training.
|
||||
"""
|
||||
Process action sequences with noise injection for training.
|
||||
|
||||
Args:
|
||||
action_chunk: Action sequences [batch, seq_len, action_dim]
|
||||
@@ -349,7 +352,8 @@ _Qwen2_5_VLForAction_Base = Qwen2_5_VLForConditionalGeneration if _wallx_deps_av
|
||||
|
||||
|
||||
class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
"""Qwen2.5 Vision-Language Mixture of Experts model for action processing.
|
||||
"""
|
||||
Qwen2.5 Vision-Language Mixture of Experts model for action processing.
|
||||
|
||||
This model extends the base Qwen2.5 VL model with action token processing capabilities
|
||||
and optional LoRA fine-tuning support.
|
||||
@@ -380,7 +384,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
strict: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Load model from pretrained model path.
|
||||
"""
|
||||
Load model from pretrained model path.
|
||||
|
||||
Args:
|
||||
pretrained_model_path (str): Model directory path containing model.safetensors file
|
||||
@@ -483,7 +488,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
flow_loss_weight=1.0,
|
||||
vision_attn_implementation: str = "auto",
|
||||
):
|
||||
"""Initialize the Qwen2.5 VLMoE model for action processing.
|
||||
"""
|
||||
Initialize the Qwen2.5 VLMoE model for action processing.
|
||||
|
||||
Args:
|
||||
config: Model configuration
|
||||
@@ -551,7 +557,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
param.data = param.data.to(torch.float32)
|
||||
|
||||
def define_action_token_id(self):
|
||||
"""Define action token IDs based on tokenizer configuration.
|
||||
"""
|
||||
Define action token IDs based on tokenizer configuration.
|
||||
|
||||
Creates mappings for fast action tokens, proprioception tokens, and general action tokens.
|
||||
"""
|
||||
@@ -574,7 +581,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
}
|
||||
|
||||
def add_lora(self, r=8, lora_alpha=32, target_modules=None, lora_dropout=0.1):
|
||||
"""Add LoRA (Low-Rank Adaptation) adapters to the model.
|
||||
"""
|
||||
Add LoRA (Low-Rank Adaptation) adapters to the model.
|
||||
|
||||
Args:
|
||||
r (int): Rank of adaptation
|
||||
@@ -630,7 +638,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
second_per_grid_ts: torch.Tensor | None = None,
|
||||
attention_mask: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Calculate 3D RoPE (Rotary Position Embedding) indices for vision and text tokens.
|
||||
"""
|
||||
Calculate 3D RoPE (Rotary Position Embedding) indices for vision and text tokens.
|
||||
|
||||
This method computes position embeddings that account for the temporal, height, and width
|
||||
dimensions of vision tokens (images/videos) while maintaining standard 1D position embeddings
|
||||
@@ -831,7 +840,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
agent_pos_mask: torch.FloatTensor | None = None,
|
||||
**kwargs,
|
||||
) -> tuple | Qwen2_5_VLACausalLMOutputWithPast:
|
||||
"""Forward pass for training with multi-modal inputs including vision, text, and action data.
|
||||
"""
|
||||
Forward pass for training with multi-modal inputs including vision, text, and action data.
|
||||
|
||||
This method handles the complete forward pass during training, processing various input modalities
|
||||
including images, videos, text, proprioceptive data, and action sequences. It computes losses
|
||||
@@ -1076,7 +1086,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
)
|
||||
|
||||
def predict_action(self, predict_mode: str, **kwargs):
|
||||
"""Predict actions using specified prediction mode.
|
||||
"""
|
||||
Predict actions using specified prediction mode.
|
||||
|
||||
Args:
|
||||
predict_mode (str): Prediction mode, either "fast" or "diffusion"
|
||||
@@ -1123,7 +1134,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
re_generate: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Multi-modal prediction method supporting text generation, fast action prediction, and diffusion-based action prediction.
|
||||
"""
|
||||
Multi-modal prediction method supporting text generation, fast action prediction, and diffusion-based action prediction.
|
||||
|
||||
This method handles three prediction modes:
|
||||
1. "text": Pure text generation using autoregressive decoding
|
||||
@@ -1411,7 +1423,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
dof_mask = dof_mask.to(inputs_embeds.device).to(torch.float32)
|
||||
|
||||
def step(timestep, noisy_action):
|
||||
"""Single denoising step for diffusion process.
|
||||
"""
|
||||
Single denoising step for diffusion process.
|
||||
|
||||
Args:
|
||||
timestep: Current diffusion timestep
|
||||
@@ -1481,7 +1494,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
return output
|
||||
|
||||
def forward(self, mode: str | None = None, predict_mode: str | None = "text", **kwargs):
|
||||
"""Main forward pass dispatcher for different execution modes.
|
||||
"""
|
||||
Main forward pass dispatcher for different execution modes.
|
||||
|
||||
This method routes execution to appropriate forward functions based on the specified mode:
|
||||
- No mode (None): Training step with gradient disabled
|
||||
@@ -1533,7 +1547,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
agent_pos_mask=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Prepare inputs for autoregressive generation with multi-modal support.
|
||||
"""
|
||||
Prepare inputs for autoregressive generation with multi-modal support.
|
||||
|
||||
This method handles input preparation for generation, including proper slicing of inputs
|
||||
based on cache position, MoE token type management, and multi-modal data handling.
|
||||
@@ -1640,7 +1655,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
self,
|
||||
input_ids: torch.LongTensor | None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Get the number of images and videos for each sample to calculate tensor separation lengths.
|
||||
"""
|
||||
Get the number of images and videos for each sample to calculate tensor separation lengths.
|
||||
|
||||
These parameters are computed directly from input_ids rather than being passed through
|
||||
the processor to avoid unpredictable impacts from interface modifications.
|
||||
@@ -1676,7 +1692,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
input_ids: torch.LongTensor | None = None,
|
||||
**model_kwargs,
|
||||
) -> tuple[torch.LongTensor, dict[str, Any]]:
|
||||
"""Expand inputs for generation with support for multi-modal tensors.
|
||||
"""
|
||||
Expand inputs for generation with support for multi-modal tensors.
|
||||
|
||||
This is an overridden method that supports expanding tensors without a standard batch
|
||||
size dimension, specifically for vision-related tensors:
|
||||
@@ -1793,7 +1810,8 @@ class Qwen2_5_VLMoEForAction(_Qwen2_5_VLForAction_Base): # noqa: N801
|
||||
|
||||
|
||||
class WallXPolicy(PreTrainedPolicy):
|
||||
"""Wall-X policy for cross-embodiment robotic control.
|
||||
"""
|
||||
Wall-X policy for cross-embodiment robotic control.
|
||||
|
||||
Integrates Qwen2.5-VL vision-language model with action prediction
|
||||
using flow matching for continuous action spaces.
|
||||
@@ -1803,13 +1821,6 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
name = "wall_x"
|
||||
|
||||
def __init__(self, config: WallXConfig, **kwargs):
|
||||
"""Load the pretrained Wall-X (Qwen2.5-VL + flow-matching action head) model and reset the action queue.
|
||||
|
||||
Args:
|
||||
config (WallXConfig): Policy configuration; also validated/completed via
|
||||
`config.validate_features()`.
|
||||
kwargs: Unused; accepted for interface compatibility with `PreTrainedPolicy`.
|
||||
"""
|
||||
require_package("transformers", extra="wallx")
|
||||
require_package("peft", extra="wallx")
|
||||
require_package("torchdiffeq", extra="wallx")
|
||||
@@ -1831,13 +1842,13 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue."""
|
||||
"""Reset action queue."""
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
}
|
||||
|
||||
def get_optim_params(self):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`]. Returns all model parameters."""
|
||||
"""Get parameters for optimization."""
|
||||
return self.parameters()
|
||||
|
||||
def preprocess_inputs(
|
||||
@@ -1846,21 +1857,20 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
*,
|
||||
compute_position_ids: bool = False,
|
||||
) -> BatchFeature:
|
||||
"""Convert a batch of LeRobot dataset items to the Wall-X model input format.
|
||||
"""
|
||||
Convert a batch of LeRobot dataset items to Wall-X model input format.
|
||||
|
||||
This processes a batched dictionary where tensors have the batch dimension first.
|
||||
This processes a batched dictionary where tensors have batch dimension first.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Dictionary with batched tensors, keyed by e.g.
|
||||
`"observation.state"` (shape `(batch_size, state_dim)` or
|
||||
`(batch_size, n_obs_steps, state_dim)`), `"action"` (shape
|
||||
`(batch_size, chunk_size, action_dim)`), `"observation.images.<key>"` (shape
|
||||
`(batch_size, C, H, W)`), and `"task"` (a list of `batch_size` strings).
|
||||
compute_position_ids (bool, *optional*, defaults to `False`): Whether to also compute and
|
||||
include RoPE position ids in the returned model inputs.
|
||||
batch: Dictionary with batched tensors:
|
||||
- "observation.state": (batch_size, state_dim) or (batch_size, n_obs_steps, state_dim)
|
||||
- "action": (batch_size, chunk_size, action_dim)
|
||||
- "observation.images.<key>": (batch_size, C, H, W)
|
||||
- "task": List[str] of length batch_size
|
||||
|
||||
Returns:
|
||||
`BatchFeature`: The batched Wall-X model inputs.
|
||||
BatchFeature containing batched model inputs
|
||||
"""
|
||||
use_fast_tokenizer = self.config.use_fast_tokenizer
|
||||
|
||||
@@ -2034,11 +2044,17 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
return inputs
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
"""
|
||||
Training forward pass using Qwen2_5_VLMoEForAction.
|
||||
|
||||
Runs `preprocess_inputs` on `batch`, then delegates to the underlying
|
||||
`Qwen2_5_VLMoEForAction` model's `forward` in `mode="train"` to compute the flow-matching
|
||||
and/or cross-entropy losses.
|
||||
Args:
|
||||
batch: Dictionary containing preprocessed inputs from preprocess_inputs()
|
||||
Expected keys: input_ids, attention_mask, pixel_values, image_grid_thw,
|
||||
proprioception, agent_pos_mask, action_chunk, dof_mask, moe_token_types,
|
||||
etc.
|
||||
|
||||
Returns:
|
||||
tuple: (loss, loss_dict)
|
||||
"""
|
||||
batch = self.preprocess_inputs(batch, compute_position_ids=True)
|
||||
|
||||
@@ -2066,11 +2082,7 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Delegates to the underlying `Qwen2_5_VLMoEForAction` model in `mode="predict"`, using either
|
||||
flow-matching or FAST decoding depending on `config.prediction_mode`.
|
||||
"""
|
||||
"""Predict action chunk for evaluation."""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
|
||||
@@ -2108,10 +2120,7 @@ class WallXPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`.
|
||||
"""
|
||||
"""Select single action for environment execution."""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ def make_wall_x_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Constructs pre-processor and post-processor pipelines for the Wall-X policy.
|
||||
"""
|
||||
Constructs pre-processor and post-processor pipelines for the Wall-X policy.
|
||||
|
||||
The pre-processing pipeline prepares input data for the model by:
|
||||
1. Renaming features to match pretrained configurations
|
||||
@@ -51,12 +52,13 @@ def make_wall_x_pre_post_processors(
|
||||
2. Moving data to the CPU
|
||||
|
||||
Args:
|
||||
config (`WallXConfig`): The policy's configuration, providing feature shapes/types and normalization settings.
|
||||
dataset_stats (`dict[str, dict[str, torch.Tensor]] | None`, *optional*): Dataset statistics used to initialize normalization layers.
|
||||
config: The configuration object for the Wall-X policy
|
||||
dataset_stats: A dictionary of statistics for normalization
|
||||
|
||||
Returns:
|
||||
A tuple containing the configured pre-processor and post-processor pipelines
|
||||
"""
|
||||
|
||||
steps = make_default_policy_processor_steps(config, dataset_stats)
|
||||
|
||||
input_steps = [
|
||||
@@ -77,7 +79,8 @@ def make_wall_x_pre_post_processors(
|
||||
|
||||
@ProcessorStepRegistry.register(name="wall_x_task_processor")
|
||||
class WallXTaskProcessor(ComplementaryDataProcessorStep):
|
||||
"""A processor step that ensures the task description is properly formatted for Wall-X.
|
||||
"""
|
||||
A processor step that ensures the task description is properly formatted for Wall-X.
|
||||
|
||||
This step handles task preprocessing similar to Qwen-VL requirements.
|
||||
"""
|
||||
|
||||
@@ -35,11 +35,10 @@ else:
|
||||
|
||||
|
||||
def _translate_vision_config(vision_config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Translate a vision config from the vendored Florence-2 format to the native format.
|
||||
"""Translate a vision config from the original Microsoft remote-code Florence-2 format
|
||||
(used by existing XVLA checkpoints) to the native ``transformers`` format.
|
||||
|
||||
Translates from the original Microsoft remote-code Florence-2 format (used by existing XVLA
|
||||
checkpoints) to the native ``transformers`` format. Configs already in the native format pass
|
||||
through unchanged.
|
||||
Configs already in the native format pass through unchanged.
|
||||
"""
|
||||
vision = dict(vision_config)
|
||||
model_type = vision.pop("model_type", None)
|
||||
@@ -77,149 +76,19 @@ def _translate_vision_config(vision_config: dict[str, Any]) -> dict[str, Any]:
|
||||
@PreTrainedConfig.register_subclass("xvla")
|
||||
@dataclass
|
||||
class XVLAConfig(PreTrainedConfig):
|
||||
"""Configuration class for the XVLA (Extended Vision-Language-Action) policy.
|
||||
"""
|
||||
Configuration class for the XVLA (Extended Vision-Language-Action) policy so it can
|
||||
plug into the LeRobot training stack.
|
||||
|
||||
Lets the policy plug into the LeRobot training stack. The config mirrors the knobs exposed in the
|
||||
original XVLA repository but also declares the input/output feature contract required by LeRobot.
|
||||
|
||||
Args:
|
||||
n_obs_steps (`int`, *optional*, defaults to 1):
|
||||
Number of environment steps of observation to pass to the policy. Unused by this policy,
|
||||
which always consumes the current-step observation only (`observation_delta_indices` is
|
||||
`None`).
|
||||
input_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from input feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
output_features (`dict[str, PolicyFeature] | None`, *optional*):
|
||||
Mapping from output feature name to its `PolicyFeature` (type and shape). Populated
|
||||
automatically from the dataset when not explicitly provided.
|
||||
device (`str | None`, *optional*):
|
||||
Device the policy runs on, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`. Falls back to the
|
||||
best available device if unset or unavailable.
|
||||
use_amp (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use Automatic Mixed Precision for training and evaluation.
|
||||
use_peft (`bool`, *optional*, defaults to `False`):
|
||||
Whether this policy is trained with PEFT (parameter-efficient fine-tuning) adapters.
|
||||
push_to_hub (`bool`, *optional*, defaults to `True`):
|
||||
Whether to push the trained policy to the Hugging Face Hub after training.
|
||||
repo_id (`str | None`, *optional*):
|
||||
Hugging Face Hub repository id to push the policy to, when `push_to_hub` is enabled.
|
||||
private (`bool | None`, *optional*):
|
||||
Whether to create/push the Hub repository as private.
|
||||
tags (`list[str] | None`, *optional*):
|
||||
Tags to attach to the policy's Hub model card.
|
||||
license (`str | None`, *optional*):
|
||||
License identifier to add to the policy's Hub model card.
|
||||
pretrained_path (`Path | None`, *optional*):
|
||||
Path or Hub repo id of pretrained weights to initialize the policy from. If `None`, the
|
||||
policy is initialized from scratch.
|
||||
pretrained_revision (`str | None`, *optional*):
|
||||
Hub revision (branch, tag, or commit hash) pinning the pretrained model version.
|
||||
chunk_size (`int`, *optional*, defaults to 32):
|
||||
The size of the action prediction chunk, in units of environment steps.
|
||||
n_action_steps (`int`, *optional*, defaults to 32):
|
||||
The number of action steps to run in the environment for one invocation of the policy. Must
|
||||
be no greater than `chunk_size`.
|
||||
dtype (`str`, *optional*, defaults to `"float32"`):
|
||||
Torch dtype (`"bfloat16"` or `"float32"`) the model's parameters and inputs are cast to.
|
||||
normalization_mapping (`dict[str, NormalizationMode]`, *optional*):
|
||||
Maps a feature type name (e.g. `"STATE"`, `"VISUAL"`) to the `NormalizationMode` to apply to
|
||||
it. Defaults to identity normalization for all feature types: images are already normalized
|
||||
by the ImageNet processor step, and state/action normalization is handled internally by the
|
||||
action space.
|
||||
florence_config (`dict[str, Any]`, *optional*):
|
||||
Florence-2 vision-language backbone configuration, containing `vision_config` and
|
||||
`text_config`. Accepted in either the native `transformers` format or the original
|
||||
Microsoft remote-code format used by existing XVLA checkpoints; see `get_florence_config`.
|
||||
tokenizer_name (`str`, *optional*, defaults to `"facebook/bart-large"`):
|
||||
Name or path of the tokenizer used to tokenize the language instruction.
|
||||
tokenizer_max_length (`int`, *optional*, defaults to 64):
|
||||
Maximum token length for the tokenized instruction.
|
||||
tokenizer_padding_side (`str`, *optional*, defaults to `"right"`):
|
||||
Padding side used by the tokenizer.
|
||||
pad_language_to (`str`, *optional*, defaults to `"max_length"`):
|
||||
Padding strategy passed to the tokenizer processor step.
|
||||
hidden_size (`int`, *optional*, defaults to 1024):
|
||||
Hidden dimension of the soft-prompted policy transformer head.
|
||||
depth (`int`, *optional*, defaults to 24):
|
||||
Number of transformer layers in the policy transformer head.
|
||||
num_heads (`int`, *optional*, defaults to 16):
|
||||
Number of attention heads in the policy transformer head.
|
||||
mlp_ratio (`float`, *optional*, defaults to 4.0):
|
||||
Feed-forward expansion ratio in the policy transformer head.
|
||||
num_domains (`int`, *optional*, defaults to 30):
|
||||
Number of embodiment domains supported by the domain-conditioned soft prompts.
|
||||
len_soft_prompts (`int`, *optional*, defaults to 32):
|
||||
Number of learned soft-prompt tokens per domain.
|
||||
dim_time (`int`, *optional*, defaults to 32):
|
||||
Embedding dimension for the flow-matching timestep.
|
||||
max_len_seq (`int`, *optional*, defaults to 512):
|
||||
Maximum sequence length supported by the policy transformer head's positional embeddings.
|
||||
use_hetero_proj (`bool`, *optional*, defaults to `False`):
|
||||
Whether to use domain-specific (heterogeneous) input/output projections in the policy
|
||||
transformer head.
|
||||
action_mode (`str`, *optional*, defaults to `"ee6d"`):
|
||||
Name of the action-space representation used by `build_action_space` (e.g. end-effector pose
|
||||
with 6D rotation), which determines the model's action dimensionality and pre/post-processing.
|
||||
num_denoising_steps (`int`, *optional*, defaults to 10):
|
||||
Number of flow-matching integration steps used to generate an action chunk at inference time.
|
||||
use_proprio (`bool`, *optional*, defaults to `True`):
|
||||
Whether to feed a proprioceptive robot state input to the model. Requires a state feature in
|
||||
`input_features` when enabled.
|
||||
max_state_dim (`int`, *optional*, defaults to 32):
|
||||
Dimension the proprioceptive state vector is padded (or truncated) to.
|
||||
max_action_dim (`int`, *optional*, defaults to 20):
|
||||
Maximum action dimension used for padding when `action_mode` is `"auto"`.
|
||||
domain_feature_key (`str | None`, *optional*):
|
||||
Batch key providing a per-sample domain id. Falls back to a `"domain_id"` batch key, then to
|
||||
an all-zeros domain id, when unset or absent from the batch.
|
||||
resize_imgs_with_padding (`tuple[int, int] | None`, *optional*):
|
||||
Target `(height, width)` to resize and pad input images to. `None` keeps the original
|
||||
resolution.
|
||||
num_image_views (`int | None`, *optional*):
|
||||
Total number of camera views the model expects, including padding views. `None` (the
|
||||
default) derives it from the number of image features plus `empty_cameras`; when set
|
||||
explicitly, the larger of the two is used.
|
||||
empty_cameras (`int`, *optional*, defaults to 0):
|
||||
Number of synthetic all-zero camera views added as placeholder input features, e.g. to match
|
||||
a pretrained model's expected view count.
|
||||
freeze_vision_encoder (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the Florence-2 vision encoder's parameters during training.
|
||||
freeze_language_encoder (`bool`, *optional*, defaults to `False`):
|
||||
Whether to freeze the Florence-2 language encoder's parameters during training.
|
||||
train_policy_transformer (`bool`, *optional*, defaults to `True`):
|
||||
Whether the policy transformer head's parameters (other than the soft prompts) are
|
||||
trainable.
|
||||
train_soft_prompts (`bool`, *optional*, defaults to `True`):
|
||||
Whether the domain soft-prompt parameters are trainable.
|
||||
optimizer_lr (`float`, *optional*, defaults to 0.0001):
|
||||
Base learning rate for the XVLA AdamW optimizer preset.
|
||||
optimizer_betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.99)`):
|
||||
Adam beta coefficients for the XVLA AdamW optimizer preset.
|
||||
optimizer_eps (`float`, *optional*, defaults to 1e-08):
|
||||
Epsilon for the XVLA AdamW optimizer preset.
|
||||
optimizer_weight_decay (`float`, *optional*, defaults to 0.0):
|
||||
Weight decay for the XVLA AdamW optimizer preset.
|
||||
optimizer_grad_clip_norm (`float`, *optional*, defaults to 10.0):
|
||||
Gradient-clipping norm for the XVLA AdamW optimizer preset.
|
||||
optimizer_soft_prompt_lr_scale (`float`, *optional*, defaults to 1.0):
|
||||
Scale factor applied to `optimizer_lr` for the soft-prompt parameters.
|
||||
optimizer_soft_prompt_warmup_lr_scale (`float | None`, *optional*):
|
||||
Starting scale factor for an optional soft-prompt learning-rate warmup, e.g. `0.01`. `None`
|
||||
disables the warmup.
|
||||
scheduler_warmup_steps (`int`, *optional*, defaults to 1000):
|
||||
Number of warmup steps for the cosine-decay-with-warmup learning rate scheduler preset.
|
||||
scheduler_decay_steps (`int`, *optional*, defaults to 30000):
|
||||
Number of decay steps for the cosine-decay-with-warmup learning rate scheduler preset.
|
||||
scheduler_decay_lr (`float`, *optional*, defaults to 2.5e-06):
|
||||
Final learning rate at the end of decay, for the cosine-decay-with-warmup scheduler preset.
|
||||
The config mirrors the knobs exposed in the original XVLA repository but also
|
||||
declares the input/output feature contract required by LeRobot.
|
||||
"""
|
||||
|
||||
# Input / output structure
|
||||
n_obs_steps: int = 1
|
||||
chunk_size: int = 32
|
||||
n_action_steps: int = 32
|
||||
dtype: str = "float32"
|
||||
dtype: str = "float32" # Options: "bfloat16", "float32"
|
||||
|
||||
normalization_mapping: dict[str, NormalizationMode] = field(
|
||||
default_factory=lambda: {
|
||||
@@ -252,7 +121,7 @@ class XVLAConfig(PreTrainedConfig):
|
||||
num_denoising_steps: int = 10
|
||||
use_proprio: bool = True
|
||||
max_state_dim: int = 32
|
||||
max_action_dim: int = 20
|
||||
max_action_dim: int = 20 # Maximum action dimension for padding (used by "auto" action mode)
|
||||
domain_feature_key: str | None = None
|
||||
|
||||
# Vision preprocessing
|
||||
@@ -260,12 +129,12 @@ class XVLAConfig(PreTrainedConfig):
|
||||
num_image_views: int | None = None
|
||||
empty_cameras: int = 0
|
||||
|
||||
# Freezing options for VLM components.
|
||||
# By default, VLM encoders are frozen and only policy transformer + soft prompts train.
|
||||
freeze_vision_encoder: bool = False
|
||||
freeze_language_encoder: bool = False
|
||||
train_policy_transformer: bool = True
|
||||
train_soft_prompts: bool = True
|
||||
# Freezing options for VLM components
|
||||
# By default, VLM encoders are frozen and only policy transformer + soft prompts train
|
||||
freeze_vision_encoder: bool = False # Freeze VLM vision encoder weights
|
||||
freeze_language_encoder: bool = False # Freeze VLM language encoder weights
|
||||
train_policy_transformer: bool = True # Allow policy transformer to train
|
||||
train_soft_prompts: bool = True # Allow soft prompts to train
|
||||
|
||||
# Training presets
|
||||
optimizer_lr: float = 1e-4
|
||||
@@ -273,15 +142,15 @@ class XVLAConfig(PreTrainedConfig):
|
||||
optimizer_eps: float = 1e-8
|
||||
optimizer_weight_decay: float = 0.0
|
||||
optimizer_grad_clip_norm: float = 10.0
|
||||
optimizer_soft_prompt_lr_scale: float = 1.0
|
||||
optimizer_soft_prompt_warmup_lr_scale: float | None = None
|
||||
# Soft-prompt LR settings (for optional warm-up)
|
||||
optimizer_soft_prompt_lr_scale: float = 1.0 # Scale factor for soft-prompt LR
|
||||
optimizer_soft_prompt_warmup_lr_scale: float | None = None # Start scale for warmup (e.g., 0.01)
|
||||
|
||||
scheduler_warmup_steps: int = 1_000
|
||||
scheduler_decay_steps: int = 30_000
|
||||
scheduler_decay_lr: float = 2.5e-6
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Resolve `device` (see [`~configs.PreTrainedConfig.__post_init__`]), then validate this config. Validates the vendored Florence-2 backbone configuration."""
|
||||
super().__post_init__()
|
||||
|
||||
if self.chunk_size <= 0:
|
||||
@@ -297,7 +166,8 @@ class XVLAConfig(PreTrainedConfig):
|
||||
self._florence_config_obj: Florence2Config | None = None
|
||||
|
||||
def get_florence_config(self) -> Florence2Config:
|
||||
"""Build (and cache) the native ``transformers`` Florence-2 config that backs the VLM.
|
||||
"""
|
||||
Build (and cache) the native ``transformers`` Florence-2 config that backs the VLM.
|
||||
|
||||
``florence_config`` may be given either in the native ``transformers`` format or in the
|
||||
original Microsoft remote-code format stored by existing XVLA checkpoints (e.g. with
|
||||
@@ -335,7 +205,6 @@ class XVLAConfig(PreTrainedConfig):
|
||||
return self._florence_config_obj
|
||||
|
||||
def validate_features(self) -> None:
|
||||
"""See [`~configs.PreTrainedConfig.validate_features`]."""
|
||||
if not self.image_features:
|
||||
raise ValueError("XVLA requires at least one visual feature in the inputs.")
|
||||
if self.use_proprio and self.robot_state_feature is None:
|
||||
@@ -376,7 +245,6 @@ class XVLAConfig(PreTrainedConfig):
|
||||
)
|
||||
|
||||
def get_scheduler_preset(self) -> CosineDecayWithWarmupSchedulerConfig:
|
||||
"""See [`~configs.PreTrainedConfig.get_scheduler_preset`]."""
|
||||
return CosineDecayWithWarmupSchedulerConfig(
|
||||
peak_lr=self.optimizer_lr,
|
||||
decay_lr=self.scheduler_decay_lr,
|
||||
@@ -386,15 +254,12 @@ class XVLAConfig(PreTrainedConfig):
|
||||
|
||||
@property
|
||||
def observation_delta_indices(self) -> list[int] | None:
|
||||
"""See [`~configs.PreTrainedConfig.observation_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def action_delta_indices(self) -> list[int]:
|
||||
"""See [`~configs.PreTrainedConfig.action_delta_indices`]."""
|
||||
return list(range(self.chunk_size))
|
||||
|
||||
@property
|
||||
def reward_delta_indices(self) -> list[int] | None:
|
||||
"""See [`~configs.PreTrainedConfig.reward_delta_indices`]."""
|
||||
return None
|
||||
|
||||
@@ -49,7 +49,9 @@ else:
|
||||
|
||||
|
||||
class XVLAModel(nn.Module):
|
||||
"""XVLA backbone that stitches Florence-2 embeddings with the temporal/action transformer head."""
|
||||
"""
|
||||
XVLA backbone that stitches Florence-2 embeddings with the temporal/action transformer head.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -117,12 +119,15 @@ class XVLAModel(nn.Module):
|
||||
return torch.float32
|
||||
|
||||
def _apply_dtype(self) -> None:
|
||||
"""Apply dtype casting to model components based on config."""
|
||||
"""
|
||||
Apply dtype casting to model components based on config.
|
||||
"""
|
||||
target_dtype = self._get_target_dtype()
|
||||
self.to(dtype=target_dtype)
|
||||
|
||||
def _apply_freezing(self) -> None:
|
||||
"""Freeze VLM vision and language encoders based on config options.
|
||||
"""
|
||||
Freeze VLM vision and language encoders based on config options.
|
||||
Keep only policy transformer and soft prompts trainable.
|
||||
"""
|
||||
# Freeze vision encoder
|
||||
@@ -159,7 +164,9 @@ class XVLAModel(nn.Module):
|
||||
pixel_values: torch.FloatTensor,
|
||||
image_mask: torch.Tensor,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Encode text and multi-view images via Florence2 encoder."""
|
||||
"""
|
||||
Encode text and multi-view images via Florence2 encoder.
|
||||
"""
|
||||
batch_size, num_views = pixel_values.shape[:2]
|
||||
flat_mask = image_mask.view(-1).to(dtype=torch.bool)
|
||||
flat_images = pixel_values.flatten(0, 1)
|
||||
@@ -197,7 +204,9 @@ class XVLAModel(nn.Module):
|
||||
proprio: torch.Tensor,
|
||||
action: torch.Tensor,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Forward pass for the XVLA model."""
|
||||
"""
|
||||
Forward pass for the XVLA model.
|
||||
"""
|
||||
target_dtype = self._get_target_dtype()
|
||||
image_input = image_input.to(dtype=target_dtype)
|
||||
proprio = proprio.to(dtype=target_dtype)
|
||||
@@ -269,12 +278,6 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
name = "xvla"
|
||||
|
||||
def __init__(self, config: XVLAConfig, **kwargs):
|
||||
"""Build the Florence-2-backed XVLA model from `config`.
|
||||
|
||||
Args:
|
||||
config (XVLAConfig): The policy configuration.
|
||||
kwargs: Forwarded to the base class.
|
||||
"""
|
||||
require_package("transformers", extra="xvla")
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
@@ -284,19 +287,16 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Reinitializes the action queue used by
|
||||
`select_action`.
|
||||
"""
|
||||
self._queues = {
|
||||
ACTION: deque(maxlen=self.config.n_action_steps),
|
||||
}
|
||||
|
||||
def get_optim_params(self) -> dict:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.get_optim_params`].
|
||||
"""Return trainable named parameters for optimization.
|
||||
|
||||
Returns a `{name: param}` dict of every parameter with `requires_grad=True`, keyed by its full
|
||||
parameter name so the XVLA AdamW optimizer preset can apply differential learning rates (e.g. a
|
||||
reduced learning rate for VLM parameters) by name pattern.
|
||||
Returns a dict of name -> param for all trainable parameters.
|
||||
This enables the xvla-adamw optimizer to apply differential learning rates
|
||||
based on parameter names (e.g., 1/10 LR for VLM components).
|
||||
"""
|
||||
return dict(filter(lambda kv: kv[1].requires_grad, self.named_parameters()))
|
||||
|
||||
@@ -389,15 +389,6 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
}
|
||||
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation/action tensors.
|
||||
|
||||
Returns:
|
||||
tuple[Tensor, dict]: The total loss, summed over the action space's per-term losses, and a
|
||||
dict of the individual loss terms for logging.
|
||||
"""
|
||||
inputs = self._build_model_inputs(batch)
|
||||
targets = self._prepare_action_targets(batch)
|
||||
losses = self.model(action=targets, **inputs)
|
||||
@@ -414,28 +405,12 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_action_chunk(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor: # noqa: ARG002
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
|
||||
|
||||
Args:
|
||||
batch (dict[str, Tensor]): A batch of preprocessed, normalized observation tensors.
|
||||
noise (Tensor | None, *optional*): Accepted for interface compatibility with
|
||||
`ActionSelectKwargs`; currently unused.
|
||||
|
||||
Returns:
|
||||
Tensor: The predicted action chunk, generated with `config.num_denoising_steps` steps of the
|
||||
flow-matching action head.
|
||||
"""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
return self._get_action_chunk(batch)
|
||||
|
||||
@torch.no_grad()
|
||||
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor: # noqa: ARG002
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
|
||||
|
||||
Uses an action queue populated by `predict_action_chunk`: the queue is refilled with the first
|
||||
`n_action_steps` predicted actions whenever it runs empty.
|
||||
"""
|
||||
self.eval()
|
||||
self._queues = populate_queues(self._queues, batch, exclude_keys=[ACTION])
|
||||
|
||||
@@ -461,11 +436,10 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
strict: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""See [`~policies.pretrained.PreTrainedPolicy.from_pretrained`].
|
||||
|
||||
Loads `model.safetensors` directly, remapping checkpoints saved with the old vendored
|
||||
Florence-2 module layout to the native `transformers` layout when detected, and restoring
|
||||
whichever alias of the tied encoder/shared token embedding `safetensors` deduplicated on save.
|
||||
"""
|
||||
Loads XVLA model weights with:
|
||||
- automatic prefix 'model.' added to all keys
|
||||
- skip list for layers that should remain randomly initialized
|
||||
"""
|
||||
import safetensors.torch
|
||||
|
||||
@@ -541,8 +515,7 @@ class XVLAPolicy(PreTrainedPolicy):
|
||||
|
||||
def _is_vendored_florence_state_dict(state_dict: dict[str, Tensor], prefix: str = "model.vlm.") -> bool:
|
||||
"""Detect XVLA checkpoints saved with the old vendored (Microsoft remote-code) Florence-2
|
||||
module layout by their signature keys.
|
||||
"""
|
||||
module layout by their signature keys."""
|
||||
return f"{prefix}image_projection" in state_dict or any(
|
||||
key.startswith(f"{prefix}language_model.model.") for key in state_dict
|
||||
)
|
||||
|
||||
@@ -50,7 +50,10 @@ def make_xvla_pre_post_processors(
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Build the LeRobot processor pipelines for XVLA."""
|
||||
"""
|
||||
Build the LeRobot processor pipelines for XVLA.
|
||||
"""
|
||||
|
||||
steps = make_default_policy_processor_steps(config, dataset_stats)
|
||||
|
||||
input_steps = [
|
||||
@@ -79,7 +82,8 @@ def make_xvla_pre_post_processors(
|
||||
# Custom XVLA processor steps
|
||||
@dataclass
|
||||
class LiberoProcessorStep(ObservationProcessorStep):
|
||||
"""Processes LIBERO observations into the LeRobot format.
|
||||
"""
|
||||
Processes LIBERO observations into the LeRobot format.
|
||||
|
||||
This step handles the specific observation structure from LIBERO environments,
|
||||
which includes nested robot_state dictionaries and image observations.
|
||||
@@ -99,7 +103,9 @@ class LiberoProcessorStep(ObservationProcessorStep):
|
||||
"""
|
||||
|
||||
def _process_observation(self, observation):
|
||||
"""Processes both image and robot_state observations from LIBERO."""
|
||||
"""
|
||||
Processes both image and robot_state observations from LIBERO.
|
||||
"""
|
||||
processed_obs = observation.copy()
|
||||
for key in list(processed_obs.keys()):
|
||||
if key.startswith(f"{OBS_IMAGES}."):
|
||||
@@ -135,7 +141,9 @@ class LiberoProcessorStep(ObservationProcessorStep):
|
||||
def transform_features(
|
||||
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
|
||||
) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
|
||||
"""Transforms feature keys from the LIBERO format to the LeRobot standard."""
|
||||
"""
|
||||
Transforms feature keys from the LIBERO format to the LeRobot standard.
|
||||
"""
|
||||
new_features: dict[PipelineFeatureType, dict[str, PolicyFeature]] = {}
|
||||
|
||||
# copy over non-STATE features
|
||||
@@ -158,7 +166,8 @@ class LiberoProcessorStep(ObservationProcessorStep):
|
||||
return new_features
|
||||
|
||||
def _mat_to_rotate6d(self, rot_mats: torch.Tensor) -> torch.Tensor:
|
||||
"""Convert batched rotation matrices (B, 3, 3) into 6D rotation representation (B, 6).
|
||||
"""
|
||||
Convert batched rotation matrices (B, 3, 3) into 6D rotation representation (B, 6).
|
||||
|
||||
Args:
|
||||
rot_mats (Tensor): Rotation matrices of shape (B, 3, 3)
|
||||
@@ -170,6 +179,7 @@ class LiberoProcessorStep(ObservationProcessorStep):
|
||||
TypeError: if input is not a torch tensor
|
||||
ValueError: if shape is not (B, 3, 3)
|
||||
"""
|
||||
|
||||
if not isinstance(rot_mats, torch.Tensor):
|
||||
raise TypeError(f"mat_to_rot6d expects a torch.Tensor, got {type(rot_mats)}")
|
||||
|
||||
@@ -198,7 +208,8 @@ class XVLAImageScaleProcessorStep(ProcessorStep):
|
||||
for XVLA models that expect images in uint8-like range.
|
||||
|
||||
Args:
|
||||
image_keys (`list[str] | None`, *optional*): Observation keys to apply this to. `None` applies to all image observations.
|
||||
image_keys: List of observation keys that contain images to scale.
|
||||
If None, will automatically detect keys starting with "observation.images."
|
||||
"""
|
||||
|
||||
image_keys: list[str] | None = None
|
||||
@@ -248,8 +259,9 @@ class XVLAImageToFloatProcessorStep(ProcessorStep):
|
||||
that are stored as uint8 values.
|
||||
|
||||
Args:
|
||||
image_keys (`list[str] | None`, *optional*): Observation keys to apply this to. `None` applies to all image observations.
|
||||
validate_range (`bool`, *optional*, defaults to `True`): Whether to check that input values are already in [0, 255] before converting.
|
||||
image_keys: List of observation keys that contain images to convert.
|
||||
If None, will automatically detect keys starting with "observation.images."
|
||||
validate_range: If True, validates that input values are in [0, 255] range (default: True)
|
||||
|
||||
Raises:
|
||||
ValueError: If validate_range is True and image values are not in [0, 255] range.
|
||||
@@ -322,7 +334,8 @@ class XVLAImageNetNormalizeProcessorStep(ProcessorStep):
|
||||
The normalization formula is: (image - mean) / std
|
||||
|
||||
Args:
|
||||
image_keys (`list[str] | None`, *optional*): Observation keys to apply this to. `None` applies to all image observations.
|
||||
image_keys: List of observation keys that contain images to normalize.
|
||||
If None, will automatically detect keys starting with "observation.images."
|
||||
|
||||
Raises:
|
||||
ValueError: If image values are not in the [0, 1] range.
|
||||
@@ -396,7 +409,7 @@ class XVLAAddDomainIdProcessorStep(ProcessorStep):
|
||||
which is used by XVLA to identify different robot embodiments or task domains.
|
||||
|
||||
Args:
|
||||
domain_id (`int`, *optional*, defaults to 0): The domain ID to add.
|
||||
domain_id: The domain ID to add (default: 3)
|
||||
"""
|
||||
|
||||
domain_id: int = 0
|
||||
@@ -447,8 +460,7 @@ class XVLARotation6DToAxisAngleProcessorStep(ProcessorStep):
|
||||
Final output: [target_eef (3), axis_angle (3), gripper (1)] = 7D action
|
||||
|
||||
Args:
|
||||
expected_action_dim (`int`, *optional*, defaults to 10): Expected input action dimension (supports
|
||||
6D rotation + extras).
|
||||
expected_action_dim: Expected input action dimension (default: 10, supports 6D rotation + extras)
|
||||
"""
|
||||
|
||||
expected_action_dim: int = 10
|
||||
@@ -504,7 +516,9 @@ def make_xvla_libero_pre_post_processors() -> tuple[
|
||||
PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
|
||||
PolicyProcessorPipeline[PolicyAction, PolicyAction],
|
||||
]:
|
||||
"""Build the LeRobot processor pipelines for XVLA with LIBERO environment."""
|
||||
"""
|
||||
Build the LeRobot processor pipelines for XVLA with LIBERO environment.
|
||||
"""
|
||||
pre_processor_steps: list[ProcessorStep] = []
|
||||
post_processor_steps: list[ProcessorStep] = []
|
||||
pre_processor_steps.extend(
|
||||
|
||||
@@ -29,18 +29,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BiOpenArmFollower(BimanualMixin, Robot):
|
||||
"""A bimanual pair of OpenArm follower arms driven as one robot."""
|
||||
"""A bimanual pair of OpenArm follower arms driven as one robot.
|
||||
|
||||
Args:
|
||||
config (`BiOpenArmFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = BiOpenArmFollowerConfig
|
||||
name = "bi_openarm_follower"
|
||||
|
||||
def __init__(self, config: BiOpenArmFollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`BiOpenArmFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -33,18 +33,16 @@ class BiRebotB601Follower(BimanualMixin, Robot):
|
||||
|
||||
Composes two single-arm :class:`RebotB601Follower` instances. Observation and
|
||||
action keys of each arm are namespaced with a ``left_`` / ``right_`` prefix.
|
||||
|
||||
Args:
|
||||
config (`BiRebotB601FollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = BiRebotB601FollowerConfig
|
||||
name = "bi_rebot_b601_follower"
|
||||
|
||||
def __init__(self, config: BiRebotB601FollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`BiRebotB601FollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -29,18 +29,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BiSOFollower(BimanualMixin, Robot):
|
||||
"""A bimanual pair of [SO follower arms](https://github.com/TheRobotStudio/SO-ARM100) by TheRobotStudio."""
|
||||
"""A bimanual pair of [SO follower arms](https://github.com/TheRobotStudio/SO-ARM100) by TheRobotStudio.
|
||||
|
||||
Args:
|
||||
config (`BiSOFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = BiSOFollowerConfig
|
||||
name = "bi_so_follower"
|
||||
|
||||
def __init__(self, config: BiSOFollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`BiSOFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -81,6 +81,11 @@ class EarthRoverMiniPlus(Robot):
|
||||
- Linear and angular velocity control
|
||||
- Battery and orientation telemetry
|
||||
|
||||
Args:
|
||||
config (`EarthRoverMiniPlusConfig`):
|
||||
The robot's configuration. Its `sdk_url` points at the Frodobots SDK server; there is no
|
||||
serial port, since control goes over HTTP.
|
||||
|
||||
**Attributes**:
|
||||
- **config** -- Robot configuration
|
||||
- **sdk_base_url** -- URL of the Frodobots SDK server (default: http://localhost:8000)
|
||||
@@ -90,11 +95,6 @@ class EarthRoverMiniPlus(Robot):
|
||||
name = "earthrover_mini_plus"
|
||||
|
||||
def __init__(self, config: EarthRoverMiniPlusConfig):
|
||||
"""Initialize EarthRover Mini Plus robot.
|
||||
|
||||
Args:
|
||||
config: Robot configuration including SDK URL
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.sdk_base_url = "http://localhost:8000"
|
||||
|
||||
@@ -39,18 +39,16 @@ class HopeJrArm(Robot):
|
||||
|
||||
The arm and the hand are separate robots; pair this with [`~robots.hope_jr.HopeJrHand`] for a full
|
||||
limb. See [`~robots.Robot`] for the contract every method here implements.
|
||||
|
||||
Args:
|
||||
config (`HopeJrArmConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = HopeJrArmConfig
|
||||
name = "hope_jr_arm"
|
||||
|
||||
def __init__(self, config: HopeJrArmConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`HopeJrArmConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.bus = FeetechMotorsBus(
|
||||
|
||||
@@ -63,18 +63,16 @@ class HopeJrHand(Robot):
|
||||
|
||||
Each hand is its own robot, so a two-handed setup uses two of these with different `side` values. See
|
||||
[`~robots.Robot`] for the contract every method here implements.
|
||||
|
||||
Args:
|
||||
config (`HopeJrHandConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = HopeJrHandConfig
|
||||
name = "hope_jr_hand"
|
||||
|
||||
def __init__(self, config: HopeJrHandConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`HopeJrHandConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.bus = FeetechMotorsBus(
|
||||
|
||||
@@ -41,18 +41,16 @@ class KochFollower(Robot):
|
||||
wrist-to-elbow expansion, developed by Alexander Koch from
|
||||
[Tau Robotics](https://tau-robotics.com).
|
||||
- [Koch v1.1](https://github.com/jess-moss/koch-v1-1), developed by Jess Moss.
|
||||
|
||||
Args:
|
||||
config (`KochFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = KochFollowerConfig
|
||||
name = "koch_follower"
|
||||
|
||||
def __init__(self, config: KochFollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`KochFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100
|
||||
|
||||
@@ -46,18 +46,16 @@ class LeKiwi(Robot):
|
||||
commands for the wheels.
|
||||
|
||||
To drive one of these from another machine, use [`~robots.lekiwi.LeKiwiClient`].
|
||||
|
||||
Args:
|
||||
config (`LeKiwiConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = LeKiwiConfig
|
||||
name = "lekiwi"
|
||||
|
||||
def __init__(self, config: LeKiwiConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`LeKiwiConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100
|
||||
|
||||
@@ -36,18 +36,16 @@ class LeKiwiClient(Robot):
|
||||
Presents the same [`~robots.Robot`] interface as [`~robots.lekiwi.LeKiwi`], but every observation and
|
||||
action crosses a ZMQ connection to the host process running on the robot. Calibration stays on the
|
||||
robot, so this class does not perform it.
|
||||
|
||||
Args:
|
||||
config (`LeKiwiClientConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = LeKiwiClientConfig
|
||||
name = "lekiwi_client"
|
||||
|
||||
def __init__(self, config: LeKiwiClientConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`LeKiwiClientConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
import zmq
|
||||
|
||||
self._zmq = zmq
|
||||
|
||||
@@ -40,15 +40,13 @@ class LeKiwiHost:
|
||||
|
||||
Runs on the robot's own computer, receiving actions on one socket and publishing observations on
|
||||
another.
|
||||
|
||||
Args:
|
||||
config (`LeKiwiHostConfig`):
|
||||
Ports, loop frequency and watchdog settings for the host.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LeKiwiHostConfig):
|
||||
"""Bind the command and observation sockets.
|
||||
|
||||
Args:
|
||||
config (`LeKiwiHostConfig`):
|
||||
Ports, loop frequency and watchdog settings for the host.
|
||||
"""
|
||||
self.zmq_context = zmq.Context()
|
||||
self.zmq_cmd_socket = self.zmq_context.socket(zmq.PULL)
|
||||
self.zmq_cmd_socket.setsockopt(zmq.CONFLATE, 1)
|
||||
|
||||
@@ -39,18 +39,16 @@ class OmxFollower(Robot):
|
||||
"""The [OpenMANIPULATOR-X](https://github.com/ROBOTIS-GIT/open_manipulator) follower arm.
|
||||
|
||||
Developed by Woojin Wie and Junha Cha at [ROBOTIS](https://ai.robotis.com/).
|
||||
|
||||
Args:
|
||||
config (`OmxFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = OmxFollowerConfig
|
||||
name = "omx_follower"
|
||||
|
||||
def __init__(self, config: OmxFollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`OmxFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100
|
||||
|
||||
@@ -41,18 +41,16 @@ class OpenArmFollower(Robot):
|
||||
|
||||
Uses Damiao motors in MIT control mode. See [`~robots.Robot`] for the contract every method here
|
||||
implements.
|
||||
|
||||
Args:
|
||||
config (`OpenArmFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = OpenArmFollowerConfig
|
||||
name = "openarm_follower"
|
||||
|
||||
def __init__(self, config: OpenArmFollowerConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`OpenArmFollowerConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -73,18 +73,17 @@ REACHY2_VEL = {
|
||||
|
||||
|
||||
class Reachy2Robot(Robot):
|
||||
"""[Reachy 2](https://www.pollen-robotics.com/reachy/), the humanoid by Pollen Robotics."""
|
||||
"""[Reachy 2](https://www.pollen-robotics.com/reachy/), the humanoid by Pollen Robotics.
|
||||
|
||||
Args:
|
||||
config (`Reachy2RobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = Reachy2RobotConfig
|
||||
name = "reachy2"
|
||||
|
||||
def __init__(self, config: Reachy2RobotConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`Reachy2RobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
require_package("reachy2_sdk", extra="reachy2")
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
@@ -60,18 +60,16 @@ class RebotB601Follower(Robot):
|
||||
|
||||
Motor communication is handled by the ``motorbridge`` package over a CAN bus,
|
||||
reached either through a Damiao serial bridge or a SocketCAN adapter.
|
||||
|
||||
Args:
|
||||
config (`RebotB601FollowerRobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
|
||||
config_class = RebotB601FollowerRobotConfig
|
||||
name = "rebot_b601_follower"
|
||||
|
||||
def __init__(self, config: RebotB601FollowerRobotConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`RebotB601FollowerRobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
require_package("motorbridge", extra="rebot")
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
@@ -41,6 +41,11 @@ class Robot(abc.ABC):
|
||||
... robot.send_action(action)
|
||||
```
|
||||
|
||||
Args:
|
||||
config (`RobotConfig`):
|
||||
The robot's configuration. Its `id` and `calibration_dir` decide where calibration is
|
||||
read from and written to.
|
||||
|
||||
**Attributes**:
|
||||
- **config_class** (`type[RobotConfig]`) -- The expected configuration class for this robot.
|
||||
- **name** (`str`) -- The unique robot name used to identify this robot type.
|
||||
@@ -51,13 +56,6 @@ class Robot(abc.ABC):
|
||||
name: str
|
||||
|
||||
def __init__(self, config: RobotConfig):
|
||||
"""Set up identity and calibration paths, loading an existing calibration file if there is one.
|
||||
|
||||
Args:
|
||||
config (`RobotConfig`):
|
||||
The robot's configuration. Its `id` and `calibration_dir` decide where calibration is
|
||||
read from and written to.
|
||||
"""
|
||||
self.robot_type = self.name
|
||||
self.id = config.id
|
||||
self.calibration_dir = (
|
||||
|
||||
@@ -44,6 +44,10 @@ class SOFollower(Robot):
|
||||
Actions and observations are keyed `"<motor>.pos"`; cameras named in the config appear in observations
|
||||
under their own keys. See [`~robots.Robot`] for the contract every method here implements.
|
||||
|
||||
Args:
|
||||
config (`SOFollowerRobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
|
||||
@@ -58,12 +62,6 @@ class SOFollower(Robot):
|
||||
name = "so_follower"
|
||||
|
||||
def __init__(self, config: SOFollowerRobotConfig):
|
||||
"""Build the robot from its configuration.
|
||||
|
||||
Args:
|
||||
config (`SOFollowerRobotConfig`):
|
||||
The robot's configuration. Its `port` and `cameras` determine what is connected.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
# choose normalization mode depending on config if available
|
||||
|
||||
@@ -24,17 +24,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WeightedMovingFilter:
|
||||
"""A fixed-length weighted moving average over recent samples, used to smooth IK solutions."""
|
||||
"""A fixed-length weighted moving average over recent samples, used to smooth IK solutions.
|
||||
|
||||
Args:
|
||||
weights (`Sequence[float]`):
|
||||
Per-sample weights, newest first. Their length sets the window size.
|
||||
data_size (`int`, *optional*, defaults to 14):
|
||||
Number of values in each sample.
|
||||
"""
|
||||
|
||||
def __init__(self, weights, data_size=14):
|
||||
"""Set up the filter.
|
||||
|
||||
Args:
|
||||
weights:
|
||||
Per-sample weights, newest first. Their length sets the window size.
|
||||
data_size (`int`, *optional*, defaults to 14):
|
||||
Number of values in each sample.
|
||||
"""
|
||||
self._window_size = len(weights)
|
||||
self._weights = np.array(weights)
|
||||
self._data_size = data_size
|
||||
@@ -76,15 +75,14 @@ class WeightedMovingFilter:
|
||||
|
||||
|
||||
class G1_29_ArmIK: # noqa: N801
|
||||
"""Inverse kinematics for the G1's two arms, solved together as one optimisation problem."""
|
||||
"""Inverse kinematics for the G1's two arms, solved together as one optimisation problem.
|
||||
|
||||
Args:
|
||||
unit_test (`bool`, *optional*, defaults to `False`):
|
||||
Whether to run in test mode, which visualises the solution instead of driving a robot.
|
||||
"""
|
||||
|
||||
def __init__(self, unit_test=False):
|
||||
"""Build the arm model and the IK solver.
|
||||
|
||||
Args:
|
||||
unit_test (`bool`, *optional*, defaults to `False`):
|
||||
Whether to run in test mode, which visualises the solution instead of driving a robot.
|
||||
"""
|
||||
import casadi
|
||||
import pinocchio as pin
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
@@ -136,22 +136,20 @@ class UnitreeG1(Robot):
|
||||
`is_simulation=True` to drive a MuJoCo model instead of the physical robot.
|
||||
|
||||
See [`~robots.Robot`] for the contract every method here implements.
|
||||
|
||||
Args:
|
||||
config (`UnitreeG1Config`):
|
||||
The robot's configuration, including gains, the ZMQ bridge address and whether to run
|
||||
against MuJoCo instead of hardware.
|
||||
|
||||
Raises:
|
||||
ImportError: If the `unitree_g1` extra is not installed.
|
||||
"""
|
||||
|
||||
config_class = UnitreeG1Config
|
||||
name = "unitree_g1"
|
||||
|
||||
def __init__(self, config: UnitreeG1Config):
|
||||
"""Build the robot and, if one is configured, its locomotion controller.
|
||||
|
||||
Args:
|
||||
config (`UnitreeG1Config`):
|
||||
The robot's configuration, including gains, the ZMQ bridge address and whether to run
|
||||
against MuJoCo instead of hardware.
|
||||
|
||||
Raises:
|
||||
ImportError: If the `unitree_g1` extra is not installed.
|
||||
"""
|
||||
require_package("unitree-sdk2py", extra="unitree_g1", import_name="unitree_sdk2py")
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
@@ -44,30 +44,28 @@ class LowStateMsg:
|
||||
"""
|
||||
|
||||
class MotorState:
|
||||
"""Motor state data for a single joint."""
|
||||
"""Motor state data for a single joint.
|
||||
|
||||
Args:
|
||||
data (`dict[str, Any]`):
|
||||
The motor's entry from the robot's state message.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
"""Build one motor's state from a deserialized JSON frame.
|
||||
|
||||
Args:
|
||||
data (`dict[str, Any]`):
|
||||
The motor's entry from the robot's state message.
|
||||
"""
|
||||
self.q: float = data.get("q", 0.0)
|
||||
self.dq: float = data.get("dq", 0.0)
|
||||
self.tau_est: float = data.get("tau_est", 0.0)
|
||||
self.temperature: float = data.get("temperature", 0.0)
|
||||
|
||||
class IMUState:
|
||||
"""IMU sensor data."""
|
||||
"""IMU sensor data.
|
||||
|
||||
Args:
|
||||
data (`dict[str, Any]`):
|
||||
The IMU's entry from the robot's state message.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
"""Build the IMU state from a deserialized JSON frame.
|
||||
|
||||
Args:
|
||||
data (`dict[str, Any]`):
|
||||
The IMU's entry from the robot's state message.
|
||||
"""
|
||||
self.quaternion: list[float] = data.get("quaternion", [1.0, 0.0, 0.0, 0.0])
|
||||
self.gyroscope: list[float] = data.get("gyroscope", [0.0, 0.0, 0.0])
|
||||
self.accelerometer: list[float] = data.get("accelerometer", [0.0, 0.0, 0.0])
|
||||
@@ -147,17 +145,16 @@ def ChannelFactoryInitialize(domain_id: int = 0, config: Any = None) -> None: #
|
||||
|
||||
|
||||
class ChannelPublisher:
|
||||
"""ZMQ-based publisher that sends commands to the robot server."""
|
||||
"""ZMQ-based publisher that sends commands to the robot server.
|
||||
|
||||
Args:
|
||||
topic (`str`):
|
||||
The topic name to publish under.
|
||||
msg_type (`type`):
|
||||
The message class this topic carries.
|
||||
"""
|
||||
|
||||
def __init__(self, topic: str, msg_type: type) -> None:
|
||||
"""Bind the publisher to a topic.
|
||||
|
||||
Args:
|
||||
topic (`str`):
|
||||
The topic name to publish under.
|
||||
msg_type (`type`):
|
||||
The message class this topic carries.
|
||||
"""
|
||||
self.topic = topic
|
||||
self.msg_type = msg_type
|
||||
|
||||
@@ -175,17 +172,16 @@ class ChannelPublisher:
|
||||
|
||||
|
||||
class ChannelSubscriber:
|
||||
"""ZMQ-based subscriber that receives state from the robot server."""
|
||||
"""ZMQ-based subscriber that receives state from the robot server.
|
||||
|
||||
Args:
|
||||
topic (`str`):
|
||||
The topic name to receive from.
|
||||
msg_type (`type`):
|
||||
The message class this topic carries.
|
||||
"""
|
||||
|
||||
def __init__(self, topic: str, msg_type: type) -> None:
|
||||
"""Bind the subscriber to a topic.
|
||||
|
||||
Args:
|
||||
topic (`str`):
|
||||
The topic name to receive from.
|
||||
msg_type (`type`):
|
||||
The message class this topic carries.
|
||||
"""
|
||||
self.topic = topic
|
||||
self.msg_type = msg_type
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ quantile statistics (q01, q10, q50, q90, q99) in their metadata. This script:
|
||||
3. If missing, computes quantile statistics for all features
|
||||
4. Updates the dataset metadata with the new quantile statistics
|
||||
|
||||
Statistics are accumulated into a single running histogram per feature across
|
||||
all episodes rather than aggregating per-episode quantile summaries. The
|
||||
resulting quantiles are histogram approximations, subject to discretization and
|
||||
range-rebinning error; image/video frames are sampled by default.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
@@ -34,9 +39,7 @@ python src/lerobot/scripts/augment_dataset_quantile_stats.py \
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -49,11 +52,10 @@ from lerobot.datasets import (
|
||||
CODEBASE_VERSION,
|
||||
DEFAULT_QUANTILES,
|
||||
LeRobotDataset,
|
||||
aggregate_stats,
|
||||
get_feature_stats,
|
||||
write_stats,
|
||||
)
|
||||
from lerobot.datasets.compute_stats import sample_indices
|
||||
from lerobot.datasets.compute_stats import RunningQuantileStats, sample_indices
|
||||
from lerobot.utils.utils import init_logging
|
||||
|
||||
|
||||
@@ -79,20 +81,25 @@ def has_quantile_stats(stats: dict[str, dict] | None, quantile_list_keys: list[s
|
||||
return False
|
||||
|
||||
|
||||
def process_single_episode(dataset: LeRobotDataset, episode_idx: int, use_sampling: bool = True) -> dict:
|
||||
"""Process a single episode and return its statistics.
|
||||
def collect_episode_arrays(
|
||||
dataset: LeRobotDataset,
|
||||
episode_idx: int,
|
||||
use_sampling: bool = True,
|
||||
skip_images: bool = False,
|
||||
) -> dict[str, tuple[np.ndarray, int]]:
|
||||
"""Collect one episode's frames per feature, flattened to (num_samples, dim).
|
||||
|
||||
Args:
|
||||
dataset: The LeRobot dataset
|
||||
episode_idx: Index of the episode to process
|
||||
use_sampling: If True, sub-sample image/video frames per episode to bound
|
||||
memory. If False, use every frame (exact, higher memory).
|
||||
episode_idx: Index of the episode to read
|
||||
use_sampling: If True, sub-sample image/video frames to bound memory.
|
||||
If False, use every frame (higher memory).
|
||||
skip_images: If True, skip image/video features entirely.
|
||||
|
||||
Returns:
|
||||
Dictionary containing episode statistics
|
||||
Mapping of feature name to that episode's values and the number of frames
|
||||
they came from (which differs from the row count for image features).
|
||||
"""
|
||||
logging.info(f"Computing stats for episode {episode_idx}")
|
||||
|
||||
start_idx = dataset.meta.episodes[episode_idx]["dataset_from_index"]
|
||||
end_idx = dataset.meta.episodes[episode_idx]["dataset_to_index"]
|
||||
|
||||
@@ -102,7 +109,9 @@ def process_single_episode(dataset: LeRobotDataset, episode_idx: int, use_sampli
|
||||
# numeric columns are cheap, so read them in full (exact).
|
||||
image_keys = [k for k in dataset.features if dataset.features[k]["dtype"] in ("image", "video")]
|
||||
numeric_keys = [
|
||||
k for k in dataset.features if dataset.features[k]["dtype"] not in ("image", "video", "string")
|
||||
k
|
||||
for k in dataset.features
|
||||
if dataset.features[k]["dtype"] not in ("image", "video", "string", "language")
|
||||
]
|
||||
|
||||
collected_data: dict[str, list] = {}
|
||||
@@ -114,7 +123,7 @@ def process_single_episode(dataset: LeRobotDataset, episode_idx: int, use_sampli
|
||||
collected_data[key] = [torch.as_tensor(v) for v in numeric_cols[key]]
|
||||
|
||||
# Image/video features: decode only a sampled subset of frames.
|
||||
if image_keys:
|
||||
if image_keys and not skip_images:
|
||||
sampled_offsets = sample_indices(episode_len) if use_sampling else list(range(episode_len))
|
||||
for offset in sampled_offsets:
|
||||
item = dataset[start_idx + offset]
|
||||
@@ -122,87 +131,82 @@ def process_single_episode(dataset: LeRobotDataset, episode_idx: int, use_sampli
|
||||
if key in item:
|
||||
collected_data.setdefault(key, []).append(item[key])
|
||||
|
||||
ep_stats = {}
|
||||
episode_arrays: dict[str, tuple[np.ndarray, int]] = {}
|
||||
for key, data_list in collected_data.items():
|
||||
if dataset.features[key]["dtype"] == "string":
|
||||
continue
|
||||
|
||||
data = torch.stack(data_list).cpu().numpy()
|
||||
if dataset.features[key]["dtype"] in ["image", "video"]:
|
||||
if data.dtype == np.uint8:
|
||||
data = data.astype(np.float32) / 255.0
|
||||
|
||||
axes_to_reduce = (0, 2, 3)
|
||||
keepdims = True
|
||||
# (N, C, H, W) -> (N * H * W, C) so quantiles are computed per channel.
|
||||
channels = data.shape[1]
|
||||
values = data.transpose(0, 2, 3, 1).reshape(-1, channels)
|
||||
else:
|
||||
axes_to_reduce = 0
|
||||
keepdims = data.ndim == 1
|
||||
values = data.reshape(-1, data.shape[-1]) if data.ndim > 1 else data.reshape(-1, 1)
|
||||
episode_arrays[key] = (values, len(data_list))
|
||||
|
||||
ep_stats[key] = get_feature_stats(
|
||||
data, axis=axes_to_reduce, keepdims=keepdims, quantile_list=DEFAULT_QUANTILES
|
||||
)
|
||||
|
||||
if dataset.features[key]["dtype"] in ["image", "video"]:
|
||||
ep_stats[key] = {
|
||||
k: v if k == "count" else np.squeeze(v, axis=0) for k, v in ep_stats[key].items()
|
||||
}
|
||||
|
||||
return ep_stats
|
||||
return episode_arrays
|
||||
|
||||
|
||||
def compute_quantile_stats_for_dataset(dataset: LeRobotDataset, use_sampling: bool = True) -> dict[str, dict]:
|
||||
"""Compute quantile statistics for all episodes in the dataset.
|
||||
def compute_quantile_stats_for_dataset(
|
||||
dataset: LeRobotDataset,
|
||||
use_sampling: bool = True,
|
||||
skip_images: bool = False,
|
||||
) -> dict[str, dict]:
|
||||
"""Compute whole-dataset statistics with one running histogram per feature.
|
||||
|
||||
Args:
|
||||
dataset: The LeRobot dataset to compute statistics for
|
||||
use_sampling: If True, sub-sample image/video frames per episode to bound
|
||||
memory. If False, use every frame (exact, higher memory).
|
||||
memory. If False, use every frame (higher memory).
|
||||
skip_images: If True, skip image/video features and leave their stats untouched.
|
||||
|
||||
Returns:
|
||||
Dictionary containing aggregated statistics with quantiles
|
||||
Dictionary containing statistics with histogram-based global quantile estimates
|
||||
|
||||
Note:
|
||||
Video decoding operations are not thread-safe, so we process episodes sequentially
|
||||
when video keys are present. For datasets without videos, we use parallel processing
|
||||
with ThreadPoolExecutor for better performance.
|
||||
Episodes are accumulated sequentially because the running accumulators are
|
||||
shared across all of them.
|
||||
"""
|
||||
logging.info(f"Computing quantile statistics for dataset with {dataset.num_episodes} episodes")
|
||||
|
||||
episode_stats_list = []
|
||||
has_videos = len(dataset.meta.video_keys) > 0
|
||||
running_stats: dict[str, RunningQuantileStats] = {}
|
||||
frame_counts: dict[str, int] = {}
|
||||
row_counts: dict[str, int] = {}
|
||||
# Kept only while a feature has a single row, so it can still be finalized.
|
||||
single_row_arrays: dict[str, np.ndarray] = {}
|
||||
|
||||
if has_videos:
|
||||
logging.info("Dataset contains video keys - using sequential processing for thread safety")
|
||||
for episode_idx in tqdm(range(dataset.num_episodes), desc="Processing episodes"):
|
||||
ep_stats = process_single_episode(dataset, episode_idx, use_sampling)
|
||||
episode_stats_list.append(ep_stats)
|
||||
else:
|
||||
logging.info("Dataset has no video keys - using parallel processing for better performance")
|
||||
max_workers = min(dataset.num_episodes, int(os.environ.get("LEROBOT_STATS_MAX_WORKERS", 16)))
|
||||
for episode_idx in tqdm(range(dataset.num_episodes), desc="Processing episodes"):
|
||||
episode_arrays = collect_episode_arrays(
|
||||
dataset, episode_idx, use_sampling=use_sampling, skip_images=skip_images
|
||||
)
|
||||
for key, (array, num_frames) in episode_arrays.items():
|
||||
running_stats.setdefault(key, RunningQuantileStats()).update(array)
|
||||
frame_counts[key] = frame_counts.get(key, 0) + num_frames
|
||||
row_counts[key] = row_counts.get(key, 0) + len(array)
|
||||
if row_counts[key] < 2:
|
||||
single_row_arrays[key] = array
|
||||
else:
|
||||
single_row_arrays.pop(key, None)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_to_episode = {
|
||||
executor.submit(process_single_episode, dataset, episode_idx, use_sampling): episode_idx
|
||||
for episode_idx in range(dataset.num_episodes)
|
||||
}
|
||||
|
||||
episode_results = {}
|
||||
with tqdm(total=dataset.num_episodes, desc="Processing episodes") as pbar:
|
||||
for future in concurrent.futures.as_completed(future_to_episode):
|
||||
episode_idx = future_to_episode[future]
|
||||
ep_stats = future.result()
|
||||
episode_results[episode_idx] = ep_stats
|
||||
pbar.update(1)
|
||||
|
||||
for episode_idx in range(dataset.num_episodes):
|
||||
if episode_idx in episode_results:
|
||||
episode_stats_list.append(episode_results[episode_idx])
|
||||
|
||||
if not episode_stats_list:
|
||||
if not running_stats:
|
||||
raise ValueError("No episode data found for computing statistics")
|
||||
|
||||
logging.info(f"Aggregating statistics from {len(episode_stats_list)} episodes")
|
||||
return aggregate_stats(episode_stats_list)
|
||||
aggregated_stats: dict[str, dict] = {}
|
||||
for key, accumulator in running_stats.items():
|
||||
if row_counts[key] < 2:
|
||||
# Histograms need at least two samples; mirror get_feature_stats' basic-stats path.
|
||||
stats = get_feature_stats(single_row_arrays[key], axis=0, keepdims=False)
|
||||
else:
|
||||
stats = accumulator.get_statistics()
|
||||
if dataset.features[key]["dtype"] in ["image", "video"]:
|
||||
# Image stats are stored as (C, 1, 1) to broadcast over height and width.
|
||||
stats = {k: v if k == "count" else v[:, np.newaxis, np.newaxis] for k, v in stats.items()}
|
||||
# `get_feature_stats` counts frames, not the per-channel rows the accumulator sees.
|
||||
stats["count"] = np.array([frame_counts[key]])
|
||||
aggregated_stats[key] = stats
|
||||
|
||||
logging.info(f"Computed global histogram statistics for {len(aggregated_stats)} features")
|
||||
return aggregated_stats
|
||||
|
||||
|
||||
def augment_dataset_with_quantile_stats(
|
||||
@@ -210,6 +214,7 @@ def augment_dataset_with_quantile_stats(
|
||||
root: str | Path | None = None,
|
||||
overwrite: bool = False,
|
||||
use_sampling: bool = True,
|
||||
skip_images: bool = False,
|
||||
) -> None:
|
||||
"""Augment a dataset with quantile statistics if they are missing.
|
||||
|
||||
@@ -218,7 +223,8 @@ def augment_dataset_with_quantile_stats(
|
||||
root: Local root directory for the dataset
|
||||
overwrite: Overwrite existing quantile statistics if they already exist
|
||||
use_sampling: If True, sub-sample image/video frames per episode to bound
|
||||
memory. If False, use every frame (exact, higher memory).
|
||||
memory. If False, use every frame (higher memory).
|
||||
skip_images: If True, skip image/video features and keep their existing stats
|
||||
"""
|
||||
logging.info(f"Loading dataset: {repo_id}")
|
||||
dataset = LeRobotDataset(
|
||||
@@ -232,7 +238,13 @@ def augment_dataset_with_quantile_stats(
|
||||
|
||||
logging.info("Dataset does not contain quantile statistics. Computing them now...")
|
||||
|
||||
new_stats = compute_quantile_stats_for_dataset(dataset, use_sampling=use_sampling)
|
||||
new_stats = compute_quantile_stats_for_dataset(
|
||||
dataset, use_sampling=use_sampling, skip_images=skip_images
|
||||
)
|
||||
|
||||
if skip_images and dataset.meta.stats:
|
||||
for key, feature_stats in dataset.meta.stats.items():
|
||||
new_stats.setdefault(key, feature_stats)
|
||||
|
||||
logging.info("Updating dataset metadata with new quantile statistics")
|
||||
dataset.meta.stats = new_stats
|
||||
@@ -276,10 +288,15 @@ def main():
|
||||
"--no-sampling",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Compute stats over every frame (exact, higher memory). By default, "
|
||||
"Compute stats over every frame (higher memory). By default, "
|
||||
"image/video frames are sub-sampled per episode to bound memory."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-images",
|
||||
action="store_true",
|
||||
help="Skip image/video features and preserve their existing stats",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
root = Path(args.root) if args.root else None
|
||||
@@ -291,6 +308,7 @@ def main():
|
||||
root=root,
|
||||
overwrite=args.overwrite,
|
||||
use_sampling=not args.no_sampling,
|
||||
skip_images=args.skip_images,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ from lerobot.policies import PreTrainedPolicy, make_policy, make_pre_post_proces
|
||||
from lerobot.policies.factory import ProcessorConfigKwargs
|
||||
from lerobot.rewards import make_reward_pre_post_processors
|
||||
from lerobot.utils.collate import lerobot_collate_fn
|
||||
from lerobot.utils.constants import TRAINING_STATE_DIR
|
||||
from lerobot.utils.constants import PRETRAINED_MODEL_DIR, TRAINING_STATE_DIR
|
||||
from lerobot.utils.import_utils import _peft_available, register_third_party_plugins, require_package
|
||||
from lerobot.utils.logging_utils import AverageMeter, MetricsTracker
|
||||
from lerobot.utils.random_utils import set_seed
|
||||
@@ -95,6 +95,20 @@ else:
|
||||
|
||||
from .lerobot_eval import eval_policy_all
|
||||
|
||||
EMA_STATE_FILENAME = "ema_state.pt"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _ema_weights(ema: Any, policy: PreTrainedPolicy) -> Iterator[None]:
|
||||
"""Temporarily swap the EMA shadow weights into `policy`, restoring the live ones on exit."""
|
||||
params = list(policy.parameters())
|
||||
ema.store(params)
|
||||
ema.copy_to(params)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
ema.restore(params)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _make_eval_envs(cfg: TrainPipelineConfig) -> Iterator[dict[str, dict[int, Any]]]:
|
||||
@@ -592,6 +606,65 @@ def train(cfg: TrainPipelineConfig):
|
||||
dl_iter = cycle(dataloader)
|
||||
policy.train()
|
||||
|
||||
# EMA shadow of the policy weights (Chi et al. 2023, Diffusion Policy, section V.D). The shadow
|
||||
# lives on the main process only, which is safe under DDP where every rank holds identical
|
||||
# weights after each gradient sync. diffusers is imported lazily so the base training path does
|
||||
# not depend on it.
|
||||
ema = None
|
||||
if cfg.ema.enable:
|
||||
if parallel_dims.is_sharded:
|
||||
raise NotImplementedError(
|
||||
"--ema.enable=true is not supported with sharded training (FSDP2/HSDP/CP): the "
|
||||
"parameters are sharded across ranks. Use a replicated (DDP) or single-GPU run."
|
||||
)
|
||||
if cfg.peft is not None:
|
||||
raise NotImplementedError("--ema.enable=true is not supported together with PEFT adapters.")
|
||||
require_package("diffusers", extra="diffusion")
|
||||
if is_main_process():
|
||||
from diffusers.training_utils import EMAModel # noqa: PLC0415
|
||||
|
||||
# A constant --ema.decay is expressed through the schedule clamp: with
|
||||
# min_decay == max_decay, the warmup curve is pinned to that value at every step.
|
||||
min_decay = cfg.ema.min_decay if cfg.ema.decay is None else cfg.ema.decay
|
||||
max_decay = cfg.ema.max_decay if cfg.ema.decay is None else cfg.ema.decay
|
||||
ema = EMAModel(
|
||||
accelerator.unwrap_model(policy).parameters(),
|
||||
decay=max_decay,
|
||||
min_decay=min_decay,
|
||||
update_after_step=cfg.ema.update_after_step,
|
||||
use_ema_warmup=True,
|
||||
inv_gamma=cfg.ema.inv_gamma,
|
||||
power=cfg.ema.power,
|
||||
)
|
||||
ema.to(device)
|
||||
if cfg.ema.decay is not None:
|
||||
logging.info(
|
||||
"EMA enabled: decay=%g (constant), update_after_step=%d, use_for_eval=%s",
|
||||
cfg.ema.decay,
|
||||
cfg.ema.update_after_step,
|
||||
cfg.ema.use_for_eval,
|
||||
)
|
||||
else:
|
||||
logging.info(
|
||||
"EMA enabled: max_decay=%g, inv_gamma=%g, power=%g, update_after_step=%d, use_for_eval=%s",
|
||||
cfg.ema.max_decay,
|
||||
cfg.ema.inv_gamma,
|
||||
cfg.ema.power,
|
||||
cfg.ema.update_after_step,
|
||||
cfg.ema.use_for_eval,
|
||||
)
|
||||
if cfg.checkpoint_path is not None:
|
||||
ema_path = cfg.checkpoint_path / TRAINING_STATE_DIR / EMA_STATE_FILENAME
|
||||
if ema_path.exists():
|
||||
ema.load_state_dict(torch.load(ema_path, map_location=device, weights_only=True))
|
||||
logging.info("Resumed EMA shadow from %s", ema_path)
|
||||
else:
|
||||
logging.warning(
|
||||
"Resuming with --ema.enable=true but %s is missing; "
|
||||
"restarting the shadow from the current weights.",
|
||||
ema_path,
|
||||
)
|
||||
|
||||
train_metrics = {
|
||||
# Per-rank loss reflects only one shard of the global batch; mean recovers the loss the
|
||||
# data-parallel group is actually optimizing. grad_norm and lr are already identical on
|
||||
@@ -602,9 +675,10 @@ def train(cfg: TrainPipelineConfig):
|
||||
"lr": AverageMeter("lr", ":0.1e"),
|
||||
# Report the slowest rank for bottleneck-style timings so multi-GPU runs surface the
|
||||
# true straggler instead of rank 0's view.
|
||||
"update_s": AverageMeter("updt_s", ":.3f", reduction="max"),
|
||||
"dataloading_s": AverageMeter("data_s", ":.3f", reduction="max"),
|
||||
# Derived from the post-reduce max step time; set once per log window on the main rank.
|
||||
"preprocessing_s": AverageMeter("prep_s", ":.3f", reduction="max"),
|
||||
"update_s": AverageMeter("updt_s", ":.3f", reduction="max"),
|
||||
"step_s": AverageMeter("step_s", ":.3f", reduction="max"),
|
||||
"samples_per_s": AverageMeter("smp/s", ":.0f"),
|
||||
}
|
||||
if torch.cuda.is_available():
|
||||
@@ -634,13 +708,15 @@ def train(cfg: TrainPipelineConfig):
|
||||
)
|
||||
|
||||
for _ in range(step, cfg.steps):
|
||||
start_time = time.perf_counter()
|
||||
step_start = time.perf_counter()
|
||||
batch = next(dl_iter)
|
||||
preprocessing_start = time.perf_counter()
|
||||
train_tracker.dataloading_s = preprocessing_start - step_start
|
||||
for cam_key in dataset.meta.camera_keys:
|
||||
if cam_key in batch and batch[cam_key].dtype == torch.uint8:
|
||||
batch[cam_key] = batch[cam_key].to(dtype=torch.float32) / 255.0
|
||||
batch = preprocessor(batch)
|
||||
train_tracker.dataloading_s = time.perf_counter() - start_time
|
||||
train_tracker.preprocessing_s = time.perf_counter() - preprocessing_start
|
||||
|
||||
train_tracker, _ = update_policy(
|
||||
train_tracker,
|
||||
@@ -652,6 +728,13 @@ def train(cfg: TrainPipelineConfig):
|
||||
lr_scheduler=lr_scheduler,
|
||||
sample_weighter=sample_weighter,
|
||||
)
|
||||
train_tracker.step_s = time.perf_counter() - step_start
|
||||
|
||||
# Pull one optimizer step of the live weights into the EMA shadow (main process only).
|
||||
# The shadow tracks optimizer updates, not micro-batches: gate on the sync step under
|
||||
# gradient accumulation.
|
||||
if ema is not None and accelerator.sync_gradients:
|
||||
ema.step(accelerator.unwrap_model(policy).parameters())
|
||||
|
||||
# Note: eval and checkpoint happens *after* the `step`th training update has completed, so we
|
||||
# increment `step` here.
|
||||
@@ -668,11 +751,8 @@ def train(cfg: TrainPipelineConfig):
|
||||
# Collective reduce must run on every rank, before the main-process gate below.
|
||||
train_tracker.reduce_across_ranks()
|
||||
if is_main_process():
|
||||
# Cluster-wide throughput, derived from the already-reduced (max) step time so it
|
||||
# reflects the slowest rank — which is what actually gates the next iteration.
|
||||
step_time = train_tracker.update_s.avg + train_tracker.dataloading_s.avg
|
||||
if step_time > 0:
|
||||
train_tracker.samples_per_s = samples_per_step / step_time
|
||||
if train_tracker.step_s.avg > 0:
|
||||
train_tracker.samples_per_s = samples_per_step / train_tracker.step_s.avg
|
||||
logging.info(train_tracker)
|
||||
if wandb_logger:
|
||||
# Policy sub-losses (latent_loss, action_loss, ...) are aggregated into the
|
||||
@@ -683,6 +763,9 @@ def train(cfg: TrainPipelineConfig):
|
||||
if sample_weighter is not None:
|
||||
weighter_stats = sample_weighter.get_stats()
|
||||
wandb_log_dict.update({f"sample_weighting/{k}": v for k, v in weighter_stats.items()})
|
||||
if ema is not None and ema.cur_decay_value is not None:
|
||||
wandb_log_dict["ema/decay"] = ema.cur_decay_value
|
||||
wandb_log_dict["ema/step"] = ema.optimization_step
|
||||
wandb_logger.log_dict(wandb_log_dict, step)
|
||||
train_tracker.reset_averages()
|
||||
|
||||
@@ -727,6 +810,17 @@ def train(cfg: TrainPipelineConfig):
|
||||
accelerator=accelerator,
|
||||
)
|
||||
if is_main_process():
|
||||
if ema is not None:
|
||||
# Save the shadow for exact resume, plus a directly loadable copy of the EMA
|
||||
# weights (lerobot-eval --policy.path=<checkpoint>/pretrained_model_ema).
|
||||
torch.save(ema.state_dict(), checkpoint_dir / TRAINING_STATE_DIR / EMA_STATE_FILENAME)
|
||||
unwrapped_policy = accelerator.unwrap_model(policy)
|
||||
ema_dir = checkpoint_dir / f"{PRETRAINED_MODEL_DIR}_ema"
|
||||
with _ema_weights(ema, unwrapped_policy):
|
||||
unwrapped_policy.save_pretrained(ema_dir)
|
||||
cfg.save_pretrained(ema_dir)
|
||||
preprocessor.save_pretrained(ema_dir)
|
||||
postprocessor.save_pretrained(ema_dir)
|
||||
update_last_checkpoint(checkpoint_dir)
|
||||
if cfg.save_checkpoint_to_hub:
|
||||
push_checkpoint_to_hub(
|
||||
@@ -742,10 +836,18 @@ def train(cfg: TrainPipelineConfig):
|
||||
if is_main_process():
|
||||
step_id = get_step_identifier(step, cfg.steps)
|
||||
logging.info(f"Eval policy at step {step}")
|
||||
with _make_eval_envs(cfg) as eval_env, torch.no_grad(), accelerator.autocast():
|
||||
eval_policy_model = accelerator.unwrap_model(policy)
|
||||
# Evaluate the EMA weights when enabled: the swap happens only on the main
|
||||
# process (the other ranks wait at the barrier below) and is exactly undone
|
||||
# afterwards, so the live weights stay in sync across ranks.
|
||||
use_ema_for_eval = ema is not None and cfg.ema.use_for_eval
|
||||
if use_ema_for_eval:
|
||||
logging.info("Evaluating the EMA weights")
|
||||
weights_cm = _ema_weights(ema, eval_policy_model) if use_ema_for_eval else nullcontext()
|
||||
with weights_cm, _make_eval_envs(cfg) as eval_env, torch.no_grad(), accelerator.autocast():
|
||||
eval_info = eval_policy_all(
|
||||
envs=eval_env, # dict[suite][task_id] -> vec_env
|
||||
policy=accelerator.unwrap_model(policy),
|
||||
policy=eval_policy_model,
|
||||
env_preprocessor=env_preprocessor,
|
||||
env_postprocessor=env_postprocessor,
|
||||
preprocessor=preprocessor,
|
||||
@@ -804,6 +906,25 @@ def train(cfg: TrainPipelineConfig):
|
||||
peft_model=unwrapped if peft_model is not None else None,
|
||||
)
|
||||
|
||||
# The push above ships the live weights; when EMA is on, the weights that were
|
||||
# evaluated are the shadow, so push those too under a sibling `<repo_id>-ema` repo.
|
||||
# The shadow lives on the main process only, so this is rank-0-only by construction.
|
||||
# Non-fatal: the live model is already up if this fails.
|
||||
if ema is not None:
|
||||
ema_repo_id = f"{active_cfg.repo_id}-ema"
|
||||
orig_repo_id = unwrapped.config.repo_id
|
||||
try:
|
||||
unwrapped.config.repo_id = ema_repo_id
|
||||
with _ema_weights(ema, unwrapped):
|
||||
unwrapped.push_model_to_hub(cfg, dataset_meta=dataset.meta)
|
||||
preprocessor.push_to_hub(ema_repo_id)
|
||||
postprocessor.push_to_hub(ema_repo_id)
|
||||
logging.info("Pushed EMA weights to %s", ema_repo_id)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logging.warning("Failed to push EMA weights to %s: %s", ema_repo_id, exc)
|
||||
finally:
|
||||
unwrapped.config.repo_id = orig_repo_id
|
||||
|
||||
# Properly clean up the distributed process group
|
||||
accelerator.wait_for_everyone()
|
||||
accelerator.end_training()
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])")
|
||||
|
||||
@@ -24,7 +27,9 @@ from lerobot.scripts.augment_dataset_quantile_stats import (
|
||||
|
||||
|
||||
def _numeric_keys(dataset):
|
||||
return [k for k, v in dataset.features.items() if v["dtype"] not in ("image", "video", "string")]
|
||||
return [
|
||||
k for k, v in dataset.features.items() if v["dtype"] not in ("image", "video", "string", "language")
|
||||
]
|
||||
|
||||
|
||||
def _image_keys(dataset):
|
||||
@@ -102,3 +107,112 @@ def test_quantile_stats_present_after_compute(tmp_path, lerobot_dataset_factory)
|
||||
)
|
||||
stats = compute_quantile_stats_for_dataset(dataset, use_sampling=True)
|
||||
assert has_quantile_stats(stats)
|
||||
|
||||
|
||||
class FakeHFDataset:
|
||||
"""Minimal stand-in exposing the column slicing used by the augment script."""
|
||||
|
||||
def __init__(self, columns: dict[str, list]):
|
||||
self._columns = columns
|
||||
|
||||
def select_columns(self, keys):
|
||||
return FakeHFDataset({key: self._columns[key] for key in keys})
|
||||
|
||||
def __getitem__(self, index):
|
||||
return {key: values[index] for key, values in self._columns.items()}
|
||||
|
||||
|
||||
def test_compute_quantile_stats_skips_language_features():
|
||||
class FakeDataset:
|
||||
num_episodes = 1
|
||||
features = {
|
||||
"action": {"dtype": "float32"},
|
||||
"observation.language": {"dtype": "language"},
|
||||
}
|
||||
meta = SimpleNamespace(episodes=[{"dataset_from_index": 0, "dataset_to_index": 2}])
|
||||
hf_dataset = FakeHFDataset(
|
||||
{
|
||||
"action": [[0.0], [1.0]],
|
||||
"observation.language": [
|
||||
[{"role": "user", "content": "pick"}],
|
||||
[{"role": "assistant", "content": "done"}],
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
stats = compute_quantile_stats_for_dataset(FakeDataset())
|
||||
|
||||
assert set(stats) == {"action"}
|
||||
|
||||
|
||||
def test_compute_quantile_stats_skip_images_avoids_decoding():
|
||||
class FakeDataset:
|
||||
num_episodes = 1
|
||||
features = {
|
||||
"action": {"dtype": "float32"},
|
||||
"observation.images.cam": {"dtype": "video"},
|
||||
}
|
||||
meta = SimpleNamespace(episodes=[{"dataset_from_index": 0, "dataset_to_index": 2}])
|
||||
hf_dataset = FakeHFDataset({"action": [[0.0], [1.0]]})
|
||||
|
||||
def __getitem__(self, index):
|
||||
raise AssertionError(f"video frame {index} was decoded despite skip_images=True")
|
||||
|
||||
stats = compute_quantile_stats_for_dataset(FakeDataset(), skip_images=True)
|
||||
|
||||
assert set(stats) == {"action"}
|
||||
|
||||
|
||||
def test_compute_quantile_stats_handles_single_frame():
|
||||
class FakeDataset:
|
||||
num_episodes = 1
|
||||
features = {"action": {"dtype": "float32"}}
|
||||
meta = SimpleNamespace(episodes=[{"dataset_from_index": 0, "dataset_to_index": 1}])
|
||||
hf_dataset = FakeHFDataset({"action": [[5.0, 7.0]]})
|
||||
|
||||
stats = compute_quantile_stats_for_dataset(FakeDataset())
|
||||
|
||||
np.testing.assert_array_equal(stats["action"]["count"], np.array([1]))
|
||||
for key in ("min", "max", "mean", "q01", "q10", "q50", "q90", "q99"):
|
||||
np.testing.assert_allclose(stats["action"][key], np.array([5.0, 7.0]))
|
||||
|
||||
|
||||
def test_compute_quantile_stats_image_count_uses_frames():
|
||||
frames = [torch.zeros(3, 2, 2), torch.ones(3, 2, 2)]
|
||||
|
||||
class FakeDataset:
|
||||
num_episodes = 1
|
||||
features = {"observation.images.cam": {"dtype": "video"}}
|
||||
meta = SimpleNamespace(episodes=[{"dataset_from_index": 0, "dataset_to_index": 2}])
|
||||
hf_dataset = FakeHFDataset({})
|
||||
|
||||
def __getitem__(self, index):
|
||||
return {"observation.images.cam": frames[index]}
|
||||
|
||||
stats = compute_quantile_stats_for_dataset(FakeDataset(), use_sampling=False)
|
||||
image_stats = stats["observation.images.cam"]
|
||||
|
||||
np.testing.assert_array_equal(image_stats["count"], np.array([2]))
|
||||
assert image_stats["mean"].shape == (3, 1, 1)
|
||||
np.testing.assert_allclose(image_stats["mean"], np.full((3, 1, 1), 0.5))
|
||||
|
||||
|
||||
def test_compute_quantile_stats_accumulates_across_episodes():
|
||||
values = [[float(value)] for value in range(100)] + [[float(value)] for value in range(1000, 1010)]
|
||||
|
||||
class FakeDataset:
|
||||
num_episodes = 2
|
||||
features = {"action": {"dtype": "float32"}}
|
||||
meta = SimpleNamespace(
|
||||
episodes=[
|
||||
{"dataset_from_index": 0, "dataset_to_index": 100},
|
||||
{"dataset_from_index": 100, "dataset_to_index": 110},
|
||||
]
|
||||
)
|
||||
hf_dataset = FakeHFDataset({"action": values})
|
||||
|
||||
stats = compute_quantile_stats_for_dataset(FakeDataset())
|
||||
|
||||
np.testing.assert_array_equal(stats["action"]["count"], np.array([110]))
|
||||
expected_q90 = np.percentile(np.asarray(values), 90, axis=0)
|
||||
np.testing.assert_allclose(stats["action"]["q90"], expected_q90, atol=0.1)
|
||||
|
||||
@@ -688,7 +688,7 @@ def test_compute_episode_stats_string_features_skipped():
|
||||
|
||||
|
||||
def test_aggregate_feature_stats_with_quantiles():
|
||||
"""Test aggregating feature stats that include quantiles."""
|
||||
"""Test aggregating feature stats that include quantiles uses conservative bounds."""
|
||||
stats_ft_list = [
|
||||
{
|
||||
"min": np.array([1.0]),
|
||||
@@ -697,6 +697,9 @@ def test_aggregate_feature_stats_with_quantiles():
|
||||
"std": np.array([2.0]),
|
||||
"count": np.array([100]),
|
||||
"q01": np.array([1.5]),
|
||||
"q10": np.array([2.0]),
|
||||
"q50": np.array([5.0]),
|
||||
"q90": np.array([9.0]),
|
||||
"q99": np.array([9.5]),
|
||||
},
|
||||
{
|
||||
@@ -706,22 +709,21 @@ def test_aggregate_feature_stats_with_quantiles():
|
||||
"std": np.array([2.5]),
|
||||
"count": np.array([150]),
|
||||
"q01": np.array([2.5]),
|
||||
"q10": np.array([3.0]),
|
||||
"q50": np.array([6.0]),
|
||||
"q90": np.array([11.0]),
|
||||
"q99": np.array([11.5]),
|
||||
},
|
||||
]
|
||||
|
||||
result = aggregate_feature_stats(stats_ft_list)
|
||||
|
||||
# Should preserve quantiles
|
||||
assert "q01" in result
|
||||
assert "q99" in result
|
||||
|
||||
# Verify quantile aggregation (weighted average)
|
||||
expected_q01 = (1.5 * 100 + 2.5 * 150) / 250 # ≈ 2.1
|
||||
expected_q99 = (9.5 * 100 + 11.5 * 150) / 250 # ≈ 10.7
|
||||
|
||||
np.testing.assert_allclose(result["q01"], np.array([expected_q01]), atol=1e-6)
|
||||
np.testing.assert_allclose(result["q99"], np.array([expected_q99]), atol=1e-6)
|
||||
# Lower quantiles use min; upper quantiles use max, regardless of counts.
|
||||
np.testing.assert_allclose(result["q01"], np.array([1.5]), atol=1e-6)
|
||||
np.testing.assert_allclose(result["q10"], np.array([2.0]), atol=1e-6)
|
||||
np.testing.assert_allclose(result["q50"], np.array([5.0]), atol=1e-6)
|
||||
np.testing.assert_allclose(result["q90"], np.array([11.0]), atol=1e-6)
|
||||
np.testing.assert_allclose(result["q99"], np.array([11.5]), atol=1e-6)
|
||||
|
||||
|
||||
def test_aggregate_stats_mixed_quantiles():
|
||||
@@ -878,3 +880,60 @@ def test_fixed_quantiles_always_computed():
|
||||
for q_key in expected_quantiles:
|
||||
assert q_key in episode_stats[key]
|
||||
assert episode_stats[key][q_key].shape == (features[key]["shape"][0],)
|
||||
|
||||
|
||||
def test_aggregate_stats_incremental_resume():
|
||||
"""Verify conservative bounds remain associative across incremental additions."""
|
||||
# Start with episode 1 stats (narrow distribution)
|
||||
ep1_stats = {
|
||||
"action": {
|
||||
"min": np.array([-10.0, -5.0]),
|
||||
"max": np.array([10.0, 5.0]),
|
||||
"mean": np.array([0.0, 0.0]),
|
||||
"std": np.array([3.0, 1.5]),
|
||||
"count": np.array([500]),
|
||||
"q01": np.array([-9.0, -4.5]),
|
||||
"q99": np.array([9.0, 4.5]),
|
||||
},
|
||||
}
|
||||
|
||||
# Episode 2: wider distribution on dim 0
|
||||
ep2_stats = {
|
||||
"action": {
|
||||
"min": np.array([-30.0, -5.0]),
|
||||
"max": np.array([40.0, 6.0]),
|
||||
"mean": np.array([5.0, 0.5]),
|
||||
"std": np.array([15.0, 2.0]),
|
||||
"count": np.array([100]),
|
||||
"q01": np.array([-25.0, -4.0]),
|
||||
"q99": np.array([35.0, 5.5]),
|
||||
},
|
||||
}
|
||||
|
||||
# First aggregation: ep1 + ep2 (simulates save_episode for ep2)
|
||||
cumulative = aggregate_stats([ep1_stats, ep2_stats])
|
||||
|
||||
# q01 should take min (conservative lower bound)
|
||||
np.testing.assert_allclose(cumulative["action"]["q01"], np.array([-25.0, -4.5]))
|
||||
# q99 should take max (conservative upper bound)
|
||||
np.testing.assert_allclose(cumulative["action"]["q99"], np.array([35.0, 5.5]))
|
||||
|
||||
# Episode 3: even wider on dim 1
|
||||
ep3_stats = {
|
||||
"action": {
|
||||
"min": np.array([-8.0, -20.0]),
|
||||
"max": np.array([8.0, 25.0]),
|
||||
"mean": np.array([0.0, 3.0]),
|
||||
"std": np.array([2.0, 8.0]),
|
||||
"count": np.array([50]),
|
||||
"q01": np.array([-7.0, -18.0]),
|
||||
"q99": np.array([7.0, 22.0]),
|
||||
},
|
||||
}
|
||||
|
||||
# Second aggregation: cumulative + ep3 (simulates save_episode for ep3)
|
||||
cumulative2 = aggregate_stats([cumulative, ep3_stats])
|
||||
|
||||
# Bounds should widen monotonically
|
||||
np.testing.assert_allclose(cumulative2["action"]["q01"], np.array([-25.0, -18.0]))
|
||||
np.testing.assert_allclose(cumulative2["action"]["q99"], np.array([35.0, 22.0]))
|
||||
|
||||
@@ -79,13 +79,10 @@ class TestParallelDims:
|
||||
|
||||
|
||||
class TestEnvGuard:
|
||||
# ACCELERATE_DYNAMO_*/ACCELERATE_GRADIENT_ACCUMULATION_STEPS are silent config overrides
|
||||
# inside accelerate itself — the guard must catch them too.
|
||||
# Silent config overrides inside accelerate itself — the guard must catch them.
|
||||
_POISON = (
|
||||
"ACCELERATE_USE_FSDP",
|
||||
"FSDP_VERSION",
|
||||
"PARALLELISM_CONFIG_DP_SHARD_SIZE",
|
||||
"ACCELERATE_DYNAMO_BACKEND",
|
||||
"ACCELERATE_USE_PARALLELISM_CONFIG",
|
||||
"ACCELERATE_GRADIENT_ACCUMULATION_STEPS",
|
||||
)
|
||||
|
||||
@@ -102,7 +99,7 @@ class TestEnvGuard:
|
||||
guard_against_env_interference()
|
||||
|
||||
def test_override_acknowledges(self, monkeypatch):
|
||||
monkeypatch.setenv("FSDP_VERSION", "2")
|
||||
monkeypatch.setenv("ACCELERATE_USE_FSDP", "true")
|
||||
monkeypatch.setenv(_ENV_OVERRIDE, "1")
|
||||
guard_against_env_interference()
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from conftest import (
|
||||
make_config,
|
||||
set_seed_all,
|
||||
) # noqa: E402
|
||||
|
||||
from lerobot.policies.vla_jepa.action_head import ( # noqa: E402
|
||||
VLAJEPAActionHead,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user