Compare commits

..

5 Commits

Author SHA1 Message Date
CarolinePascal c112ba6957 docs: write the API reference docstrings for policies
Documents lerobot.policies to its Wave 3 narrow scope: PreTrainedPolicy,
PreTrainedConfig's factory (factory.py), policies/utils.py, and for each of
the 19 policy families, the full Config dataclass plus the public
forward/select_action/predict_action_chunk/get_optim_params/reset surface of
the main <Family>Policy class and the make_<family>_pre_post_processors
factory. Per-policy internals (backbone/model building blocks, nested
ProcessorStep helpers) stay out of scope and D-ignored.

Fixes several real bugs found along the way: PreTrainedPolicy.forward had a
literal `_summary_`/`_description_` placeholder docstring; DiffusionPolicy
and VQBeTPolicy's __init__ docstrings documented a nonexistent `dataset_stats`
param; XVLAPolicy.from_pretrained's docstring described a prefix-stripping
behavior the code doesn't implement; XVLAAddDomainIdProcessorStep's docstring
claimed the wrong default; a handful of dead `"""Input validation..."""`
statements sat after the first statement in `__post_init__` (never actually
docstrings) and are removed.

Adds docs/source/api/policies.mdx sections for every family's Config/Policy
pair, ratchets interrogate's fail-under from 55 to 58 (measured 59% with this
PR), and adds the new leaf modules to check_docstrings.py's MODULES_TO_CHECK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 14:46:44 +02:00
Pepijn 741005d719 docs: write the API reference docstrings
Every docstring change for the API reference, on top of the infrastructure PR
which contains none. Two halves: a repo-wide pass over what the renderer cannot
handle, and `src/lerobot/robots/` taken to 100% as the worked example.

**Renderer fixes, repo-wide.** Both of these render incorrectly the moment
`[[autodoc]]` is on, and both were verified against a local build:

- 24 Sphinx roles across three files. They are unsupported and render as literal
  `:pymeth:` text. Method references become doc-builder cross-references; the
  ones pointing at instance attributes become inline code, since attributes get
  no autodoc anchor and a cross-reference would be a dead link.
- 43 `Attributes:` sections across 27 files. doc-builder parses a bare
  `Attributes:` as a synonym for `Parameters:` — `Robot`'s attributes rendered
  inside `<paramsdesc>`, presenting `config_class` and `name` to readers as
  constructor arguments when the actual parameter is `config`. Where the
  original carried no type, the type comes from the real class annotation rather
  than being invented.

The four base classes every other module inherits from — `robot.py`,
`teleoperator.py`, `motors_bus.py`, `camera.py` — are rewritten to the standard,
since subclasses document only their deviations from that text.

Three docstring errors corrected in passing: `Teleoperator.get_action` pointed
at `observation_features`, which `Teleoperator` does not have; `send_feedback`
documented a `Returns:` for a method returning `None`; and `config_class` was
typed `RobotConfig` instead of `type[TeleoperatorConfig]`.

**`robots/`, 109/306 -> 306/306.** The configuration dataclasses were the
substantial part. Their fields were documented only with `#` comments above each
field, which doc-builder cannot see: before this, `SO101FollowerConfig` rendered
all eleven of its fields with not one description. Each config now carries an
`Args:` block on the concrete registered class, covering inherited fields too,
because doc-builder renders only a class's own docstring and several of these
configs are thin multiple-inheritance shims whose body is `pass`.

The inline comments are kept rather than removed, so fields stay annotated in
the source as well as on the rendered page. Note this leaves each field
described twice, and only the `Args:` block is checked against the signature by
`make check-docstrings`, so the two can drift.

Writing them turned up things worth stating plainly on the page rather than
leaving in a comment: which configs have no serial port at all because they talk
over a network or the cloud (Reachy 2, Unitree G1, LeKiwi's client, EarthRover),
which manage their own calibration so `calibration_dir` does nothing, that
OpenArm's default joint limits are deliberately tiny until `side` is set, and
that reBot's `port` means a different thing depending on `can_adapter`.

Two pre-existing docstring bugs that the doctest infrastructure surfaced are
fixed here: `SerialMotorsBus` used `>>>` inside a ```bash block to show CLI
output, which doctest read as Python and failed on with a SyntaxError, and
`MotorsBus.torque_disabled`'s example referenced an undefined name.
`ensure_safe_goal_position` gains a genuinely executing example so the doctest
gate is not vacuous.

**Gates ratcheted**, each of which the infrastructure PR left deliberately
loose:

- `check_docstrings.py`'s ignore list emptied — the ten objects it held all had
  bare `Attributes:` sections, now converted.
- `check_config_docstrings.py`'s ignore list emptied — every registered robot
  config documents its port and calibration semantics.
- `robots/` removed from the ruff `D` per-file-ignores, as are the two
  package-root files, whose one-line docstring issues are fixed here. D100 and
  D104 are ignored globally instead: they ask for a banner on every file and
  every `__init__.py`, which appears on no rendered page.
- `interrogate` raised 52 -> 55 against a measured 55.3%.
- The four `robots/` files carrying examples added to the doctest allowlist,
  which shipped empty.

Verified: all 66 changed files under `src/lerobot/` are provably docstring-only
(AST with docstrings stripped is byte-identical to main), no comment line is
removed anywhere in `robots/`, and 708 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:01:34 +02:00
Pepijn 2e8345a5cc docs: add API documentation infrastructure
LeRobot's documentation build passes `--not_python_module`, which tells
doc-builder there is no importable Python package and disables `[[autodoc]]`
entirely. The result is that all 90+ pages are hand-written guides and there is
no generated API reference at all.

This is the machinery to change that. It deliberately contains no docstring
changes of its own — every docstring edit lives in the follow-up PR, so this
one can be reviewed as tooling and configuration alone.

**The standard.** `docs/source/writing_docstrings.mdx` is the contract: Google
section headers with Hugging Face type formatting, the machine-checked argument
line, `**Attributes**:`, doc-builder cross-references, fenced doctest examples.
It also records three behaviours that are not discoverable from the source and
were verified against a local build: `[[autodoc]]` silently skips members with
no docstring; doc-builder does not inherit docstrings from base classes, so a
registered config shim whose body is `pass` renders every field with no
description; and module-level aliases resolve to the canonical class.

**Autodoc turned on**, with two changes that are not obvious:

- `--version main` on the main-docs job. Without `--not_python_module`,
  doc-builder resolves the version from `lerobot.__version__` and only maps it
  to the default branch when it contains "dev". transformers relies on that;
  our main carries 0.6.2. Verified by building both ways — dropping the flag
  alone would publish the main docs to /lerobot/v0.6.2/ instead of
  /lerobot/main/ and disable notebook building.
- `pre_command` on both jobs. doc-builder ships a mock-deps registry entry for
  lerobot, so the reusable workflow takes its light-install path, which cannot
  import the package. The heavy dependencies cannot be mocked either: draccus
  runs `register_subclass` at import time and `processor/converters.py` calls
  `functools.singledispatch.register(torch.Tensor)`, which needs a real class.
  `[dataset]` is the only extra required.

Workflow triggers gain `src/**`, since the reference is now generated from
docstrings. `docs/source/api/` is excluded from the prettier hook, which reads
`[[autodoc]]` member lists as lazy paragraph continuations and joins a ten-entry
list onto one line.

Nine API reference pages, scaffolded with each module's base class.

**Doctests.** `LeRobotDocTestParser` is mandatory rather than optional here:
ruff's `docstring-code-format = true` drops the blank line before a closing
fence, after which stdlib's `_EXAMPLE_RE` reads the fence as expected output and
every example with output fails. It is written against the installed pytest
rather than copied from transformers, whose version predates pytest 9's
`import_path` signature and its own fix for the `@property` line-number bug.
`preprocess_string` also diverges: the upstream fenced-block split puts a
single-line example's code in a chunk with no `>>>` in it, so neither the CUDA
skip nor the `+IGNORE_RESULT` injection fires for it.

**Checkers.** `utils/check_docstrings.py` is the ~300-line core of the
2203-line transformers original; the `@auto_docstring` system, modular
propagation, GitPython and `checkers.py` are not ported.
`utils/check_config_docstrings.py` checks that every registered robot config
documents its port and calibration semantics.

**Gates**, all set to values that pass today: ruff `D` with per-file-ignores
per unconverted module, `interrogate` at `fail-under = 52` against a measured
52.1%, and Makefile targets wired into the quality workflow. The doctest
allowlist ships empty and the `doctest` target handles that, because the files
carrying runnable examples arrive with the docstring PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:57:26 +02:00
Haoming Song ef88d4e52b feat(train): parallel training framework — FSDP2, HSDP, gradient accumulation, and DCP checkpoints (#4010)
* feat(train): parallel training engine with FSDP2, HSDP, and DCP checkpoints

Replace the FSDP1 training path with a config-owned parallel-training
engine:

- Topology and runtime configs (--parallelism.*, --accelerator.*):
  dp_replicate x dp_shard degrees select single-process, DDP (unchanged
  default), FSDP2, or HSDP; mixed precision, first-class gradient
  accumulation, and FSDP/DDP tuning knobs are mirrored as plain
  dataclasses that build the accelerate objects at runtime, so every
  run is reproducible from its train_config.json alone. Accelerate env
  vars are guarded against configuring the engine behind the config
  system's back.
- Declarative policy surface: policies declare FSDP2 wrap units
  (_fsdp_wrap_modules) and non-forward entry points
  (_fsdp_forward_methods); a shared engine resolves them around
  accelerator.prepare(). Context-parallel fields are reserved and
  validated to 1.
- Checkpoints: selectable --checkpoint_format (safetensors | dcp |
  safetensors_dcp); the sharded optimizer channel is always DCP;
  two-phase resume (step+RNG before prepare, DCP model/optimizer after)
  reshards across GPU-topology changes; lerobot-convert-dcp merges DCP
  shards into a distributable model.safetensors offline.
- Publishing: PreTrainedPolicy.push_model_to_hub is replaced by the
  free publish_trained_model (model + processors + card + train config,
  all-ranks gather with main-rank writes);
  PreTrainedPolicy._save_pretrained gathers state dicts internally,
  removing the state_dict= threading from save_pretrained.
- lerobot_train is restructured around the engine: optimizer built
  before the single prepare() call, deferred weight load on DCP
  resumes, collective save_checkpoint with no call-site rank branches,
  dp-world-size-based sample accounting.

Breaking changes: FSDP checkpoints from lerobot <= 0.6.x are not
resumable (weights stay loadable via from_pretrained; pin
lerobot==0.6.x to finish old runs); the `accelerate launch
--config_file` yaml flow is superseded by the config flags; training
autocast is owned exclusively by --accelerator.mixed_precision
(policy.dtype only casts parameters).

Also fixes: reward-model hub publishing crash (TypeError on extra
kwargs).

Verified by ~200 new CPU tests (config round-trips, checkpoint
round-trips per format, two-phase resume, publisher contracts,
converter equivalence, accelerate canaries), a 5-test 4-GPU suite
(FSDP2 save/resume bit-exactness, HSDP/DDP loss parity,
changed-topology resume, all-ranks save_pretrained, grad-accum
equivalence), and end-to-end ACT (1/4/8 GPUs) + FastWAM 6B
(FSDP2 + HSDP) training runs.
2026-08-06 19:16:41 +08:00
Pepijn 64b23178d5 feat(data): add recipe-driven language supervision (#4182)
* feat(data): add recipe-driven language supervision

* test(collate): expect preserved language columns

* Address PR review feedback

* Address Claude review feedback
2026-08-04 16:48:47 +02:00
234 changed files with 14496 additions and 3256 deletions
+21 -5
View File
@@ -24,19 +24,24 @@ on:
required: false
type: string
# Triggers the workflow on push events to main for the docs folder
# Triggers on pushes to main that touch the docs or the sources the API reference is generated from.
# `src/**` is included because the API reference is built from docstrings via `[[autodoc]]`: without it,
# published API pages would go stale as soon as a docstring changed.
push:
branches:
- main
paths:
- "docs/**"
- "src/**"
# Triggers the workflow on pull request events targeting main for the docs folder
# Same for pull requests, so a docstring change gets a preview build and a broken `[[autodoc]]` path
# fails the PR rather than main.
pull_request:
branches:
- main
paths:
- "docs/**"
- "src/**"
release:
types: [published]
@@ -59,12 +64,21 @@ jobs:
with:
commit_sha: ${{ github.sha }}
package: lerobot
# 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
# `processor/converters.py` calls `functools.singledispatch.register(torch.Tensor)`, neither of
# which works against a mock. Install the package for real before the build.
pre_command: uv pip install "./lerobot[dataset]"
# `--version main` is load-bearing: without `--not_python_module`, doc-builder falls back to
# `lerobot.__version__` and only maps that to the default branch when it contains "dev". Our main
# branch carries a release version (0.6.2), so omitting this would publish the main docs to
# /lerobot/v0.6.2/ instead of /lerobot/main/ and disable notebook building.
additional_args: >-
--not_python_module
${{
(github.event_name == 'release' && format('--version {0}', github.event.release.tag_name)) ||
(inputs.version != '' && format('--version {0}', inputs.version)) ||
''
'--version main'
}}
secrets:
token: ${{ secrets.HUGGINGFACE_PUSH }}
@@ -83,4 +97,6 @@ jobs:
commit_sha: ${{ github.event.pull_request.head.sha }}
pr_number: ${{ github.event.number }}
package: lerobot
additional_args: --not_python_module
# See the comment on build_main_docs. The PR workflow passes its own `--version pr_<n>`, so no
# additional_args are needed here.
pre_command: uv pip install "./lerobot[dataset]"
+38
View File
@@ -56,3 +56,41 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
with:
extra_args: --all-files --show-diff-on-failure --color=always
# This job runs the examples in our docstrings and validates the doctest allowlist.
# See docs/source/writing_docstrings.mdx for the standard these enforce.
doc-checks:
name: Run Documentation Checks (Doctests)
runs-on: ubuntu-latest
env:
# Examples that need a physical robot, a serial port or a Hub download are skipped by content.
# Everything else has to actually run. See src/lerobot/utils/doctest_utils.py.
SKIP_HARDWARE_DOCTEST: "1"
SKIP_CUDA_DOCTEST: "1"
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup uv and Python
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
version: "0.11.30"
python-version: "3.12"
- name: Install dependencies
run: uv sync --locked --extra test --extra dataset
- name: Check the doctest list is sorted and its paths exist
run: make check-doctest-list
- name: Check documented arguments match their signatures
run: make check-docstrings
- name: Check docstring coverage has not regressed
run: uv run --with interrogate interrogate --config=pyproject.toml
- name: Run doctests
run: make doctest
+11 -2
View File
@@ -67,7 +67,11 @@ repos:
args: [--prose-wrap=preserve]
# Jinja2 model-card templates use a .md extension but contain {% ... %} /
# {{ ... }} tags that prettier's Markdown formatter mangles (e.g. table loops).
exclude: ^src/lerobot/templates/.*\.md$
#
# docs/source/api/ holds the generated API reference. Its `[[autodoc]]` blocks restrict output
# to an indented `- member` list, which prettier reads as a lazy paragraph continuation and
# joins onto one line — silently turning a member list into part of the directive.
exclude: ^(src/lerobot/templates/.*\.md|docs/source/api/.*\.mdx)$
##### Security #####
- repo: https://github.com/gitleaks/gitleaks
@@ -104,8 +108,13 @@ repos:
# args: ["--docstring-style", "google", "-v", "2"]
# exclude: ^tests/.*$
# interrogate runs in CI (quality.yml, doc-checks job) rather than here. Its 1.7.0 release still imports
# the deprecated `py` package, which resolves against whatever `py` happens to be importable in
# pre-commit's isolated env — on a machine with miniconda on the path that is a stray `py.py` and the
# hook dies before it reads any config. The gate is the same either way; the CI step is just reliable.
# - repo: https://github.com/econchick/interrogate
# rev: 1.7.0
# hooks:
# - id: interrogate
# args: ["-vv", "--config=pyproject.toml"]
# args: ["--config=pyproject.toml"]
# pass_filenames: false
+4
View File
@@ -50,6 +50,10 @@ To run checks manually on all files:
pre-commit run --all-files
```
### Docstrings
The API reference is generated from the docstrings in `src/lerobot/`. If you add or change anything public, follow the [docstring standard](https://huggingface.co/docs/lerobot/writing_docstrings) — the format is parsed by the renderer and checked in CI.
### Running Tests
We use `pytest`. First, ensure you have test artifacts by installing **git-lfs**:
+26
View File
@@ -184,3 +184,29 @@ test-smolvla-ete-eval:
# backend, so it does not require a real model checkpoint or GPU.
annotation-e2e:
uv run python -m tests.annotations.run_e2e_smoke
# Docstring & doctest checks. See docs/source/writing_docstrings.mdx for the standard these enforce.
# Run the examples in the docstrings listed in utils/documentation_tests.txt. Hardware and GPU examples are
# skipped by content (see src/lerobot/utils/doctest_utils.py); CI sets both flags.
doctest:
@files=$$(grep -v '^\s*#' utils/documentation_tests.txt | grep -v '^\s*$$'); \
if [ -z "$$files" ]; then \
echo "utils/documentation_tests.txt lists no files; nothing to run."; \
else \
SKIP_HARDWARE_DOCTEST=1 uv run pytest --doctest-modules --no-header -q $$files; \
fi
check-doctest-list:
uv run python utils/check_doctest_list.py
fix-doctest-list:
uv run python utils/check_doctest_list.py --fix_and_overwrite
check-docstrings:
uv run python utils/check_docstrings.py
uv run python utils/check_config_docstrings.py
fix-docstrings:
uv run python utils/check_docstrings.py --fix_and_overwrite
uv run python utils/check_doctest_list.py --fix_and_overwrite
+60
View File
@@ -0,0 +1,60 @@
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Root conftest: makes doctest collection use LeRobot's parser.
This only affects `--doctest-modules` runs (see `make doctest`). The test suite itself is configured by
`tests/conftest.py`.
"""
import doctest
import _pytest.doctest
from lerobot.utils.doctest_utils import LeRobotDoctestModule, LeRobotDocTestParser
# Lets an example opt out of output comparison with `# doctest: +IGNORE_RESULT`, for calls whose output is
# a progress bar or otherwise not reproducible.
IGNORE_RESULT = doctest.register_optionflag("IGNORE_RESULT")
OutputChecker = doctest.OutputChecker
class CustomOutputChecker(OutputChecker):
"""An output checker that honours the `IGNORE_RESULT` flag."""
def check_output(self, want, got, optionflags):
"""Return `True` when `IGNORE_RESULT` is set, otherwise defer to stdlib.
Args:
want (`str`):
The expected output.
got (`str`):
The actual output.
optionflags (`int`):
Bitmask of active doctest option flags.
Returns:
`bool`: Whether the output is considered a match.
"""
if IGNORE_RESULT & optionflags:
return True
return OutputChecker.check_output(self, want, got, optionflags)
# Reassigning these module attributes is how doctest behaviour is customised; mypy sees it as assigning to
# a type, which is exactly what is intended here.
doctest.OutputChecker = CustomOutputChecker # type: ignore[misc]
_pytest.doctest.DoctestModule = LeRobotDoctestModule
doctest.DocTestParser = LeRobotDocTestParser # type: ignore[misc]
+22
View File
@@ -191,6 +191,28 @@
- sections:
- local: contributing
title: Contribute to LeRobot
- local: writing_docstrings
title: Writing docstrings
- local: backwardcomp
title: Backward compatibility
title: "About"
- sections:
- local: api/robots
title: Robots
- local: api/teleoperators
title: Teleoperators
- local: api/cameras
title: Cameras
- local: api/motors
title: Motors
- local: api/datasets
title: Datasets
- local: api/policies
title: Policies
- local: api/processor
title: Processors
- local: api/envs
title: Environments
- local: api/configs
title: Configuration
title: "API Reference"
+3 -3
View File
@@ -33,7 +33,7 @@ LeRobot provides processor steps for converting between joint and EE spaces usin
```python
from lerobot.model.kinematics import RobotKinematics
from lerobot.robots.so_follower.robot_kinematic_processor import (
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
@@ -44,7 +44,7 @@ kinematics = RobotKinematics(
)
# Joints → EE (for observations: "where is my gripper?")
fk_step = ForwardKinematicsJointsToEEObservation(kinematics=kinematics, motor_names=[...])
fk_step = ForwardKinematicsJointsToEE(kinematics=kinematics, motor_names=[...])
# EE → Joints (for actions: "move my gripper here")
ik_step = InverseKinematicsEEToJoints(kinematics=kinematics, motor_names=[...])
@@ -197,7 +197,7 @@ Here is how the different processors compose. Each arrow is a processor step, an
```
┌─────────────────────────────────────────┐
Action Space │ Joint Space ←──IK──→ EE Space │
│ ForwardKinematicsJointsToEEAction
│ ForwardKinematicsJointsToEE
│ InverseKinematicsEEToJoints │
└─────────────────────────────────────────┘
+24
View File
@@ -0,0 +1,24 @@
# Cameras
Cameras supply the image observations a policy sees. Every backend — OpenCV, Intel RealSense, Reachy 2 —
implements the [`Camera`] interface, so swapping hardware does not change the code that reads frames.
See the [Cameras guide](../cameras) for choosing and configuring a camera, and
[Third-Party Cameras & Sensors](../third_party_sensors) for devices outside the core set.
## Camera
[[autodoc]] lerobot.cameras.Camera
- connect
- disconnect
- read
- async_read
- find_cameras
## CameraConfig
[[autodoc]] lerobot.cameras.CameraConfig
## make_cameras_from_configs
[[autodoc]] lerobot.cameras.make_cameras_from_configs
+27
View File
@@ -0,0 +1,27 @@
# Configuration
LeRobot configuration is plain dataclasses parsed by [draccus](https://github.com/dlwh/draccus), so every
field is settable from the CLI. [`TrainPipelineConfig`] is the top-level object for `lerobot-train`.
Polymorphic configs (policies, robots, environments) use `draccus.ChoiceRegistry`: a subclass registers
itself with `@register_subclass("name")` and is then selectable by that name on the command line.
## TrainPipelineConfig
[[autodoc]] lerobot.configs.train.TrainPipelineConfig
## PreTrainedConfig
[[autodoc]] lerobot.configs.PreTrainedConfig
## DatasetConfig
[[autodoc]] lerobot.configs.DatasetConfig
## EvalConfig
[[autodoc]] lerobot.configs.EvalConfig
## WandBConfig
[[autodoc]] lerobot.configs.WandBConfig
+23
View File
@@ -0,0 +1,23 @@
# Datasets
[`LeRobotDataset`] is the format every LeRobot script reads and writes. It is episode-aware, decodes video
observations on the fly, and round-trips to the Hugging Face Hub.
See [Using LeRobotDataset](../lerobot-dataset-v3) for the format and the common operations,
[Porting Large Datasets](../porting_datasets_v3) for migration, and [Tools](../tools) for the CLI.
## LeRobotDataset
[[autodoc]] lerobot.datasets.LeRobotDataset
## LeRobotDatasetMetadata
[[autodoc]] lerobot.datasets.LeRobotDatasetMetadata
## MultiLeRobotDataset
[[autodoc]] lerobot.datasets.MultiLeRobotDataset
## StreamingLeRobotDataset
[[autodoc]] lerobot.datasets.StreamingLeRobotDataset
+19
View File
@@ -0,0 +1,19 @@
# Environments
Simulation environments are configured through [`EnvConfig`] and built by [`make_env`]. Each subclass
declares its `gym_kwargs` and how to construct the vectorised environments.
See [Environments from the Hub](../envhub) for using published environments and
[Adding a New Benchmark](../adding_benchmarks) for contributing one.
## EnvConfig
[[autodoc]] lerobot.envs.EnvConfig
## make_env
[[autodoc]] lerobot.envs.make_env
## make_env_config
[[autodoc]] lerobot.envs.make_env_config
+23
View File
@@ -0,0 +1,23 @@
# Motors
`MotorsBus` is the low-level interface to a chain of servos on a serial bus. Robots use it to read positions
and write goal positions; you rarely touch it directly unless you are adding hardware.
See [Bring Your Own Hardware](../integrate_hardware) for adding a new bus, and
[Updating Feetech Firmware](../feetech) and [Damiao Motors and CAN Bus](../damiao) for device-specific notes.
## MotorsBus
[[autodoc]] lerobot.motors.motors_bus.MotorsBus
## Motor
[[autodoc]] lerobot.motors.Motor
## MotorCalibration
[[autodoc]] lerobot.motors.MotorCalibration
## MotorNormMode
[[autodoc]] lerobot.motors.MotorNormMode
+177
View File
@@ -0,0 +1,177 @@
# Policies
Every policy inherits [`PreTrainedPolicy`], which combines a `torch.nn.Module` with the Hub mixin, so any
policy can be pushed to and loaded from the Hugging Face Hub with the same two calls.
Each policy has its own guide with training recipes and results — [ACT](../act), [SmolVLA](../smolvla),
[π₀](../pi0), [π₀.₅](../pi05) and the rest are listed under Policies. To add one, see
[Adding a Policy](../bring_your_own_policies).
## 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
[[autodoc]] lerobot.configs.PreTrainedConfig
## 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
+20
View File
@@ -0,0 +1,20 @@
# Processors
Processors are the data transformation layer between a robot, a dataset and a policy. A pipeline is a chain
of [`ProcessorStep`]s; each step declares how it transforms both the data and the feature contract.
See [Introduction to Robot Processors](../introduction_processors) for the concepts,
[Implement your own processor](../implement_your_own_processor) to write a step, and
[Debug your processor pipeline](../debug_processor_pipeline) when a pipeline misbehaves.
## ProcessorStep
[[autodoc]] lerobot.processor.pipeline.ProcessorStep
## DataProcessorPipeline
[[autodoc]] lerobot.processor.pipeline.DataProcessorPipeline
## PolicyProcessorPipeline
[[autodoc]] lerobot.processor.pipeline.PolicyProcessorPipeline
+147
View File
@@ -0,0 +1,147 @@
# Robots
Every robot in LeRobot implements the [`Robot`] interface: connect, read an observation, send an action,
disconnect. Writing a policy or a recording script against that interface means it works with any supported
arm without change.
This page is the generated reference. For wiring, calibration and first-run instructions, start with the
hardware guides — [SO-101](../so101), [LeKiwi](../lekiwi), [Hope Jr](../hope_jr), [Reachy 2](../reachy2),
[OpenArm](../openarm) — or [Imitation Learning for Robots](../il_robots) for the end-to-end workflow. To add
a robot of your own, see [Bring Your Own Hardware](../integrate_hardware).
## Robot
The abstract base class. Subclasses implement every method below; the contract described here is what a
policy or recording loop can rely on.
[[autodoc]] lerobot.robots.Robot
- connect
- disconnect
- configure
- calibrate
- get_observation
- send_action
- observation_features
- action_features
- is_connected
- is_calibrated
## RobotConfig
[[autodoc]] lerobot.robots.RobotConfig
## make_robot_from_config
[[autodoc]] lerobot.robots.make_robot_from_config
## SO-100 and SO-101 followers
`SO100Follower` and `SO101Follower` are aliases of the same `SOFollower` class; the two arms differ in their
configuration, not their control code. `SO100FollowerConfig` and `SO101FollowerConfig` are likewise aliases
of `SOFollowerRobotConfig`.
[[autodoc]] lerobot.robots.so_follower.SOFollower
- all
[[autodoc]] lerobot.robots.so_follower.SOFollowerRobotConfig
## BiSOFollower
Two SO followers driven as one bimanual robot.
[[autodoc]] lerobot.robots.bi_so_follower.BiSOFollower
- all
[[autodoc]] lerobot.robots.bi_so_follower.BiSOFollowerConfig
## KochFollower
[[autodoc]] lerobot.robots.koch_follower.KochFollower
- all
[[autodoc]] lerobot.robots.koch_follower.KochFollowerConfig
## LeKiwi
`LeKiwi` runs on the robot itself. `LeKiwiClient` is the host-side proxy that talks to it over the network
and presents the same [`Robot`] interface.
[[autodoc]] lerobot.robots.lekiwi.LeKiwi
- all
[[autodoc]] lerobot.robots.lekiwi.LeKiwiConfig
[[autodoc]] lerobot.robots.lekiwi.LeKiwiClient
- all
[[autodoc]] lerobot.robots.lekiwi.LeKiwiClientConfig
## OpenArmFollower
[[autodoc]] lerobot.robots.openarm_follower.OpenArmFollower
- all
[[autodoc]] lerobot.robots.openarm_follower.OpenArmFollowerConfig
## BiOpenArmFollower
[[autodoc]] lerobot.robots.bi_openarm_follower.BiOpenArmFollower
- all
[[autodoc]] lerobot.robots.bi_openarm_follower.BiOpenArmFollowerConfig
## OmxFollower
[[autodoc]] lerobot.robots.omx_follower.OmxFollower
- all
[[autodoc]] lerobot.robots.omx_follower.OmxFollowerConfig
## Reachy2Robot
[[autodoc]] lerobot.robots.reachy2.Reachy2Robot
- all
[[autodoc]] lerobot.robots.reachy2.Reachy2RobotConfig
## UnitreeG1
[[autodoc]] lerobot.robots.unitree_g1.UnitreeG1
- all
[[autodoc]] lerobot.robots.unitree_g1.UnitreeG1Config
## Hope Jr
The Hope Jr humanoid is exposed as two independent robots, an arm and a hand.
[[autodoc]] lerobot.robots.hope_jr.HopeJrArm
- all
[[autodoc]] lerobot.robots.hope_jr.HopeJrArmConfig
[[autodoc]] lerobot.robots.hope_jr.HopeJrHand
- all
[[autodoc]] lerobot.robots.hope_jr.HopeJrHandConfig
## RebotB601Follower
[[autodoc]] lerobot.robots.rebot_b601_follower.RebotB601Follower
- all
[[autodoc]] lerobot.robots.rebot_b601_follower.RebotB601FollowerRobotConfig
## BiRebotB601Follower
[[autodoc]] lerobot.robots.bi_rebot_b601_follower.BiRebotB601Follower
- all
[[autodoc]] lerobot.robots.bi_rebot_b601_follower.BiRebotB601FollowerConfig
## EarthRoverMiniPlus
[[autodoc]] lerobot.robots.earthrover_mini_plus.EarthRoverMiniPlus
- all
[[autodoc]] lerobot.robots.earthrover_mini_plus.EarthRoverMiniPlusConfig
+30
View File
@@ -0,0 +1,30 @@
# Teleoperators
A teleoperator produces actions for a robot to follow — a leader arm, a gamepad, a keyboard, a phone. All of
them implement the [`Teleoperator`] interface, so a recording script written against it works with any input
device.
See [Phone teleoperation](../phone_teleop) and [Isaac Teleop](../isaac_teleop) for setup guides, and
[Imitation Learning for Robots](../il_robots) for the recording workflow.
## Teleoperator
[[autodoc]] lerobot.teleoperators.Teleoperator
- connect
- disconnect
- configure
- calibrate
- get_action
- send_feedback
- action_features
- feedback_features
- is_connected
- is_calibrated
## TeleoperatorConfig
[[autodoc]] lerobot.teleoperators.TeleoperatorConfig
## make_teleoperator_from_config
[[autodoc]] lerobot.teleoperators.make_teleoperator_from_config
+12 -2
View File
@@ -161,6 +161,16 @@ The methods called by the train/eval loops:
Batches are flat dictionaries keyed by the constants in [`lerobot.utils.constants`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/utils/constants.py): `OBS_STATE` (`observation.state.<motor>`), `OBS_IMAGES` (`observation.images.<camera>`), `OBS_LANGUAGE`, `ACTION`, etc. Reuse the constants — don't invent new prefixes.
If your model is large enough to warrant [sharded multi-GPU training](./multi_gpu_training#sharded-training-fsdp), also declare its FSDP wrap units — the repeated block classes sharding operates on:
```python
class MyPolicy(PreTrainedPolicy):
...
_fsdp_wrap_modules = ["MyTransformerBlock"]
```
With this one declaration, `--parallelism.dp_shard=N` works out of the box for your policy (users can still override it with `--accelerator.fsdp.wrap_modules`). Without any wrap source, sharded runs fail at startup by design.
### Processor functions
LeRobot uses `PolicyProcessorPipeline`s to normalize inputs and de-normalize outputs around your policy. For a concrete reference, see [`processor_act.py`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/act/processor_act.py) or [`processor_diffusion.py`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/diffusion/processor_diffusion.py).
@@ -300,7 +310,7 @@ The file names are load-bearing: the factory does lazy imports by name, and the
Two places need to know about your policy. All by name.
1. **`policies/__init__.py`** — re-export `MyPolicyConfig` and add it to `__all__`. This import is what registers your policy: `@PreTrainedConfig.register_subclass("my_policy")` runs, and from then on the factory resolves everything by convention. **Don't** re-export the modeling class; it loads lazily through the factory (so `import lerobot` stays fast).
2. **`templates/lerobot_modelcard_template.md` and the root `README.md`** — the template is what `push_model_to_hub` renders into the model card of every checkpoint trained with your policy: add a one-line description of your policy in the `model_name` branches, map it in `policy_docs` so cards link to your MDX guide, and optionally add an architecture image to `diagrams`. Then add your policy to the models table in the root `README.md`, under the right category, linking to your doc page.
2. **`templates/lerobot_modelcard_template.md` and the root `README.md`** — the template is what the end-of-training publisher renders into the model card of every checkpoint trained with your policy: add a one-line description of your policy in the `model_name` branches, map it in `policy_docs` so cards link to your MDX guide, and optionally add an architecture image to `diagrams`. Then add your policy to the models table in the root `README.md`, under the right category, linking to your doc page.
Mirror an existing policy that's structurally similar to yours; the diff is small.
@@ -344,7 +354,7 @@ A new policy is much easier to review — and far more useful — when it ships
**Pick at least one in-tree benchmark.** LeRobot ships sim benchmarks with per-benchmark Docker images (LIBERO, LIBERO-plus, Meta-World, RoboTwin 2.0, RoboCasa365, RoboCerebra, RoboMME, VLABench and more). Pick the one that matches your policy's modality — VLAs usually go to LIBERO or VLABench; image-only BC to LIBERO or Meta-World. The full list lives under [Benchmarks](./libero) in the docs sidebar.
**Push the checkpoint & processors** to the Hub under `lerobot/<policy>_<benchmark>` (or your namespace if you don't have write access; a maintainer can mirror it). Use `PreTrainedPolicy.push_model_to_hub` so the repo gets `config.json`, `model.safetensors`, and a model card.
**Push the checkpoint & processors** to the Hub under `lerobot/<policy>_<benchmark>` (or your namespace if you don't have write access; a maintainer can mirror it). The easiest way is training with `--policy.repo_id=<namespace>/<repo>` and `--policy.push_to_hub=true`: `lerobot-train` publishes the model, both processors, and a model card at the end of the run. To publish an existing checkpoint after the fact, upload its `pretrained_model/` directory (e.g. `huggingface-cli upload`), or use `lerobot-convert-dcp --push_to_hub=...` for sharded-format checkpoints.
**Report results in your policy's MDX**, with the exact `lerobot-eval` command and hardware so anyone can re-run:
+2 -2
View File
@@ -145,7 +145,7 @@ The environment processor (`env_processor`) handles incoming observations and en
1. **VanillaObservationProcessorStep**: Converts raw robot observations into standardized format
2. **JointVelocityProcessorStep** (optional): Adds joint velocity information to observations
3. **MotorCurrentProcessorStep** (optional): Adds motor current readings to observations
4. **ForwardKinematicsJointsToEEObservation** (optional): Computes end-effector pose from joint positions
4. **ForwardKinematicsJointsToEE** (optional): Computes end-effector pose from joint positions
5. **ImageCropResizeProcessorStep** (optional): Crops and resizes camera images
6. **TimeLimitProcessorStep** (optional): Enforces episode time limits
7. **GripperPenaltyProcessorStep** (optional): Applies penalties for inappropriate gripper usage
@@ -413,7 +413,7 @@ We support using a gamepad or a keyboard or the leader arm of the robot.
HIL-Serl learns actions in the end-effector space of the robot. Therefore, the teleoperation will control the end-effector's x,y,z displacements.
The end-effector transformation is applied by the processor pipeline (`EEReferenceAndDelta`, `EEBoundsAndSafety`, `GripperVelocityToJoint`, `InverseKinematicsEEToJoints`, `AddIKSolutionStep`) configured under `env.processor.inverse_kinematics` (`InverseKinematicsConfig`) and `env.processor.gripper` / `env.processor.max_gripper_pos`. The defaults related to the end-effector space are:
The end-effector transformation is applied by the processor pipeline (`InverseKinematicsRLStep`, `EEBoundsAndSafety`, `EEReferenceAndDelta`, `GripperVelocityToJoint`) configured under `env.processor.inverse_kinematics` (`InverseKinematicsConfig`) and `env.processor.gripper` / `env.processor.max_gripper_pos`. The defaults related to the end-effector space are:
<!-- prettier-ignore-start -->
```python
+19 -3
View File
@@ -108,6 +108,7 @@ own binding plus a matching image block, e.g.
```yaml
ask_vqa_top:
route: vqa
bindings:
vqa_query: "emitted_at(t, style=vqa, role=user, camera=observation.images.top)"
vqa: "emitted_at(t, style=vqa, role=assistant, camera=observation.images.top)"
@@ -127,7 +128,9 @@ ask_vqa_top:
}
```
Add one such sub-recipe per camera the dataset records.
Add one such sub-recipe per camera the dataset records. The explicit
`route: vqa` marker makes a matching sparse VQA annotation take precedence
over normal weighted blend selection; component names are purely descriptive.
## Layer 3 — training format
@@ -141,7 +144,20 @@ sample["target_message_indices"]
The renderer does not apply a tokenizer chat template. Policy processors decide how to serialize the messages for their backbone, which keeps the same dataset usable across SmolVLA, Pi0.5, and any future VLM that expects OpenAI-style chat messages.
## Blends
Blend recipes select one weighted sub-recipe deterministically from the sample index.
`recipes/subtask_mem.yaml` trains the compact core blend — high-level subtask prediction, low-level execution, and memory. `recipes/subtask_mem_vqa_speech.yaml` is the fuller variant that also adds VQA and spoken interjection responses.
`recipes/subtask_joint.yaml` demonstrates joint sequence training rather than a
weighted blend. For the same sample, its assistant subtask is supervised with
text cross-entropy on the `low_level` stream while action prediction remains
active, matching the joint setup from the π0.5 paper. Enable
`--policy.joint_subtask_conditioning=true` to use that subtask conditioning at inference.
## Graceful absence
If both language columns are missing, `None`, or empty, `RenderMessagesStep` is a no-op.
If an event-scoped branch is selected on a frame without the required event row, rendering returns `None`, allowing a loader to retry another sample.
If both language columns are missing, `None`, or empty, `RenderMessagesStep` uses
the task string as low-level supervision when available and otherwise leaves the
sample unchanged. For an annotated sample, if no recipe branch applies and no
task fallback exists, rendering returns `None`, allowing a loader to retry another sample.
+114 -118
View File
@@ -1,28 +1,29 @@
# Multi-GPU Training
This guide shows you how to train policies on multiple GPUs using [Hugging Face Accelerate](https://huggingface.co/docs/accelerate).
LeRobot trains on multiple GPUs through [Hugging Face Accelerate](https://huggingface.co/docs/accelerate). Three data-parallel layouts are supported:
| Layout | What it does | Config |
| -------- | ------------------------------------------------------------- | ------------------------------------------------------- |
| **DDP** | Replicates the full model on every GPU | default on any multi-GPU launch |
| **FSDP** | Shards parameters, gradients, and optimizer state across GPUs | `--parallelism.dp_shard=N` |
| **HSDP** | Shards within groups of GPUs, replicates across groups | `--parallelism.dp_replicate=R --parallelism.dp_shard=S` |
## Installation
`accelerate` is included in the `training` extra. Install it with:
`accelerate` is included in the `training` extra:
```bash
pip install 'lerobot[training]'
```
## Training with Multiple GPUs
## Launching
You can launch training in two ways:
Distributed training can be launched through both `torchrun` and `accelerate launch`. Accelerate is used as a plain launcher: it does not manage the training configuration, and every distributed training setting lives in LeRobot's own config system.
### Option 1: Without config (specify parameters directly)
You can specify all parameters directly in the command without running `accelerate config`:
With `torchrun`:
```bash
accelerate launch \
--multi_gpu \
--num_processes=2 \
$(which lerobot-train) \
torchrun --nproc-per-node=2 $(which lerobot-train) \
--dataset.repo_id=${HF_USER}/my_dataset \
--policy.type=act \
--policy.repo_id=${HF_USER}/my_trained_policy \
@@ -31,32 +32,10 @@ accelerate launch \
--wandb.enable=true
```
**Key accelerate parameters:**
- `--multi_gpu`: Enable multi-GPU training
- `--num_processes=2`: Number of GPUs to use
- `--mixed_precision=fp16`: Use fp16 mixed precision (or `bf16` if supported)
### Option 2: Using accelerate config
If you prefer to save your configuration, you can optionally configure accelerate for your hardware setup by running:
With `accelerate launch` (as a plain launcher):
```bash
accelerate config
```
This interactive setup will ask you questions about your training environment (number of GPUs, mixed precision settings, etc.) and saves the configuration for future use. For a simple multi-GPU setup on a single machine, you can use these recommended settings:
- Compute environment: This machine
- Number of machines: 1
- Number of processes: (number of GPUs you want to use)
- GPU ids to use: (leave empty to use all)
- Mixed precision: fp16 or bf16 (recommended for faster training)
Then launch training with:
```bash
accelerate launch $(which lerobot-train) \
accelerate launch --num_processes=2 $(which lerobot-train) \
--dataset.repo_id=${HF_USER}/my_dataset \
--policy.type=act \
--policy.repo_id=${HF_USER}/my_trained_policy \
@@ -65,116 +44,133 @@ accelerate launch $(which lerobot-train) \
--wandb.enable=true
```
## How It Works
With no `--parallelism.*` flags, a multi-process launch runs plain DDP. Multi-node runs use the standard `torchrun --nnodes/--node-rank/--rdzv-endpoint` flags (or `accelerate launch --num_machines/--machine_rank/--main_process_ip`).
When you launch training with accelerate:
> [!WARNING]
> Accelerate's YAML config files (`accelerate launch --config_file some.yaml`, `accelerate config`) are not supported. They configure the engine through environment variables, bypassing LeRobot's configuration system, so `train_config.json` would no longer describe the settings a run actually used. `lerobot-train` therefore refuses to start when [accelerate environment variables](https://huggingface.co/docs/accelerate/usage_guides/fsdp) are set. Put the settings in `--parallelism.*` / `--accelerator.*` flags instead, or set `LEROBOT_ALLOW_ACCELERATE_ENV=1` to acknowledge the override and proceed anyway.
1. **Automatic detection**: LeRobot automatically detects if it's running under accelerate
2. **Data distribution**: Your batch is automatically split across GPUs
3. **Gradient synchronization**: Gradients are synchronized across GPUs during backpropagation
4. **Single process logging**: Only the main process logs to wandb and saves checkpoints
## Batch semantics, learning rate, and steps
## Learning Rate and Training Steps Scaling
Each of the `dp_replicate × dp_shard` data-parallel workers loads its own `--batch_size` micro-batch every step, so one training step consumes `batch_size × dp_world_size` samples, and `× gradient_accumulation_steps` of those go into each optimizer update:
**Important:** LeRobot does **NOT** automatically scale learning rates or training steps based on the number of GPUs. This gives you full control over your training hyperparameters.
### Why No Automatic Scaling?
Many distributed training frameworks automatically scale the learning rate by the number of GPUs (e.g., `lr = base_lr × num_gpus`).
However, LeRobot keeps the learning rate exactly as you specify it.
### When and How to Scale
If you want to scale your hyperparameters when using multiple GPUs, you should do it manually:
**Learning Rate Scaling:**
```bash
# Example: 2 GPUs with linear LR scaling
# Base LR: 1e-4, with 2 GPUs -> 2e-4
accelerate launch --num_processes=2 $(which lerobot-train) \
--optimizer.lr=2e-4 \
--dataset.repo_id=lerobot/pusht \
--policy.type=act
```
effective_batch_size = batch_size × dp_world_size × gradient_accumulation_steps
```
**Training Steps Scaling:**
The training banner prints this factorization at startup. `--steps` counts loop steps (micro-batches per worker), not optimizer updates.
Since the effective batch size `bs` increases with multiple GPUs (batch_size × num_gpus), you may want to reduce the number of training steps proportionally:
Gradient accumulation is a first-class flag:
```bash
# Example: 2 GPUs with effective batch size 2x larger
# Original: batch_size=8, steps=100000
# With 2 GPUs: batch_size=8 (16 in total), steps=50000
accelerate launch --num_processes=2 $(which lerobot-train) \
--batch_size=8 \
--steps=50000 \
--dataset.repo_id=lerobot/pusht \
--policy.type=act
torchrun --nproc-per-node=2 $(which lerobot-train) \
--batch_size=8 --accelerator.gradient_accumulation.steps=4 ...
```
## Training Large Models with FSDP
**LeRobot does not auto-scale the learning rate or the number of steps** when the effective batch size grows. If you scale out and want equivalent training, please adjust manually, e.g. with 2 GPUs: double `--optimizer.lr` (linear scaling), or halve `--steps`.
DDP replicates the full model on every GPU, so a model that doesn't fit on one GPU won't fit under
DDP either. For large models, use **FSDP** (Fully Sharded Data Parallel), which shards parameters,
gradients, and optimizer state across GPUs. See the [accelerate FSDP guide](https://huggingface.co/docs/accelerate/usage_guides/fsdp) for background.
## Sharded training (FSDP)
An example on how to launch LeRobot training with FSDP across 4 GPUs (1 machine):
If a model is too large to train with DDP, shard it with FSDP2:
```bash
accelerate launch --config_file fsdp.yaml --num_processes=4 $(which lerobot-train) \
torchrun --nproc-per-node=4 $(which lerobot-train) \
--dataset.repo_id=${HF_USER}/my_dataset \
--policy.type=<your_policy> \
--parallelism.dp_shard=4 \
--accelerator.mixed_precision=bf16 \
--output_dir=outputs/train/my_policy_fsdp
```
A minimal `fsdp.yaml` (FSDP1; shards params/grads/optimizer — ZeRO-3-equivalent):
`--parallelism.dp_shard=-1` shards over however many processes the launcher started.
```yaml
compute_environment: LOCAL_MACHINE
distributed_type: FSDP
mixed_precision: bf16
num_machines: 1
num_processes: 4
fsdp_config:
fsdp_version: 1
fsdp_sharding_strategy: FULL_SHARD # params + grads + optimizer (ZeRO-3)
fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
fsdp_transformer_layer_cls_to_wrap: <YourTransformerBlock> # repeated block class to shard
fsdp_use_orig_params: true # required: optimizer is built pre-prepare
fsdp_state_dict_type: FULL_STATE_DICT
### Wrap units
FSDP shards the model in units (typically the repeated transformer block) and gathers one unit at a time during forward/backward. Policies declare their wrap units via `_fsdp_wrap_modules` on the policy class. For example, ACT declares `["ACTEncoderLayer", "ACTDecoderLayer"]` and FastWAM declares `["MoTLayer"]`. For a policy without a `_fsdp_wrap_modules` declaration, pass one of the flags below. You can specify the module class name explicitly, or use a size-based policy instead:
```bash
--accelerator.fsdp.wrap_modules='["MyTransformerBlock"]' # explicit class names
--accelerator.fsdp.min_num_params=1000000 # or: wrap every submodule above 1M params
```
Set `fsdp_transformer_layer_cls_to_wrap` to your model's repeated transformer-block class so each
block is sharded as its own unit. `fsdp_use_orig_params: true` is required because LeRobot builds the
optimizer before `accelerator.prepare()`.
If a policy doesn't declare `_fsdp_wrap_modules` and no `--accelerator.fsdp.wrap_modules` or `--accelerator.fsdp.min_num_params` is passed, the run fails at startup rather than silently wrapping only the root module (which would forfeit all sharding memory savings).
### FSDP checkpoints
Other sharding settings:
LeRobot gathers the full state dict across all ranks and the main process writes it as a single
`model.safetensors`, loadable as usual with `Policy.from_pretrained(...)`. Two things to look out for:
- `--accelerator.fsdp.reshard_after_forward`: whether to keep each unit's parameters resident after forward.
- `--accelerator.fsdp.cpu_offload`: keeps parameters, gradients and optimizer states on CPU.
- `--accelerator.fsdp.ignored_modules`: a regex of module paths to keep unsharded.
- **Checkpoints store fp32 weights.** Under mixed precision (`bf16`/`fp16`) FSDP keeps an fp32 master
copy, and the checkpoint saves it (~2× the bf16 size on disk) so training can resume consistently
with the fp32 optimizer state; `from_pretrained` casts back to the policy dtype on load. FSDP-specific
caveat: an fp32 checkpoint is materialized in full precision on the target device _before_ casting,
so loading it for inference on a tight GPU can OOM even when the bf16 model would fit — load on CPU
first, or cast `model.safetensors` to the deployment dtype offline.
- The sharded optimizer state is gathered into a full (world-size-independent) state dict and saved
alongside the model in the same `optimizer_state.safetensors` / `optimizer_param_groups.json`
format as single-GPU training, so **resume-from-checkpoint is supported** with `--resume=true`.
Resume reshards both the model and the optimizer state to the _current_ FSDP topology, so you can
resume an FSDP checkpoint on a different number of GPUs. Note that the data sampler is only
sample-exact when the world size and batch size match the original run (a warning is logged
otherwise); the optimizer/model state itself is unaffected.
### HSDP
Hybrid Sharded Data Parallel: parameters, gradients and optimizer states are sharded across `dp_shard` ranks, and that sharding is replicated `dp_replicate` times. Parameter all-gathers and gradient reduce-scatters stay inside a shard group; only the all-reduce that synchronizes the replicas crosses between groups. The two degrees must multiply to the world size:
```bash
# 16 GPUs = 2 nodes × 8: shard within each node, replicate across nodes
torchrun --nnodes=2 --nproc-per-node=8 ... $(which lerobot-train) \
--parallelism.dp_replicate=2 --parallelism.dp_shard=8 ...
```
## Checkpoints
Every checkpoint contains a `pretrained_model/` directory and a `training_state/` directory:
```text
005000/ # the training step at that checkpoint
├── pretrained_model/
│ ├── config.json # policy config
│ ├── train_config.json # the full training config
│ ├── model.safetensors # full weights (checkpoint_format ∈ {safetensors, safetensors_dcp}, or any non-sharded run)
│ ├── pytorch_model_fsdp_0/ # DCP weight shards (checkpoint_format ∈ {dcp, safetensors_dcp})
│ ├── policy_preprocessor.json # preprocessor config (when the run has a preprocessor)
│ ├── policy_preprocessor_step_*.safetensors # state of the stateful preprocessor steps
│ ├── policy_postprocessor.json # postprocessor config (when the run has a postprocessor)
│ └── policy_postprocessor_step_*.safetensors # state of the stateful postprocessor steps
└── training_state/
├── training_step.json # step counter, topology, and batch semantics
├── rng_state.safetensors # rng states
├── scheduler_state.json # scheduler state (when the run has a scheduler)
├── optimizer_state.safetensors # full optimizer state (non-sharded runs)
├── optimizer_param_groups.json # optimizer param groups (non-sharded runs)
└── optimizer_0/ # DCP optimizer shards (sharded runs)
```
During single-GPU or DDP training, the pipeline serializes each state dict into a single file: `model.safetensors` for the model and `optimizer_state.safetensors` for the optimizer.
During sharded training, the optimizer state is saved as DCP shards under `training_state/optimizer_0/`, and the layout of the model under `pretrained_model/` can be configured through `--checkpoint_format`:
| `--checkpoint_format` | Weights artifact | Use when |
| ------------------------- | -------------------------------------------- | --------------------------------------------------------------------- |
| `safetensors` _(default)_ | single `model.safetensors` only | you want every checkpoint immediately loadable with `from_pretrained` |
| `dcp` | `pytorch_model_fsdp_0/` shard directory only | gathering the full weights makes saves and resumes too slow |
| `safetensors_dcp` | both | you want fast resume _and_ immediately loadable checkpoints |
Two things to know about gathered (`safetensors`) checkpoints from sharded runs:
- **They store fp32 weights.** Under mixed precision training, FSDP keeps an fp32 master copy, and the checkpoint saves the master copy to make sure training resumes consistently.
- The gather is collective (all ranks participate) but only the main process writes.
### Converting DCP checkpoints
`lerobot-convert-dcp` merges a DCP shard directory into a regular `model.safetensors`, offline and without GPUs:
```bash
lerobot-convert-dcp --checkpoint_dir=outputs/train/run/checkpoints/005000
lerobot-convert-dcp --checkpoint_dir=... --delete_dcp=true --push_to_hub=${HF_USER}/my_policy
```
`--push_to_hub` publishes the converted directory as a model repo.
### Resuming
Resume with `--resume=true --config_path=.../checkpoints/last/pretrained_model/train_config.json`. Resuming from a DCP checkpoint supports resharding the model and optimizer state to the _current_ topology, which means you can resume with a different `dp_replicate/dp_shard` split. The data sampler can always resume at the right epoch and offset, but is only _sample-exact_ when the world size and batch size match the original run (a warning is logged otherwise).
> [!NOTE]
> FSDP checkpoints written by LeRobot 0.6.x and earlier used a different on-disk layout (a gathered full optimizer state) and **cannot be resumed**.
## Notes
- The `--policy.use_amp` flag in `lerobot-train` is only used when **not** running with accelerate. When using accelerate, mixed precision is controlled by accelerate's configuration.
- Training logs, checkpoints, and hub uploads are only done by the main process to avoid conflicts. Non-main processes have console logging disabled to prevent duplicate output.
- The effective batch size is `batch_size × num_gpus`. If you use 4 GPUs with `--batch_size=8`, your effective batch size is 32.
- Learning rate scheduling is handled correctly across multiple processes—LeRobot sets `step_scheduler_with_optimizer=False` to prevent accelerate from adjusting scheduler steps based on the number of processes.
- When saving or pushing models, LeRobot automatically unwraps the model from accelerate's distributed wrapper to ensure compatibility.
- WandB integration automatically initializes only on the main process, preventing multiple runs from being created.
- Checkpoint saves and end-of-training publishes are collective (every rank enters them). Gathered weights, sidecar files and Hub uploads are written by the main process alone.
- Metrics are reduced across ranks before logging: losses are averaged, and `samples/s` reports cluster-wide throughput.
- Learning-rate scheduling is stepped once per training step regardless of the number of processes (`step_scheduler_with_optimizer=False` is baked in).
For more advanced configurations and troubleshooting, see the [Accelerate documentation](https://huggingface.co/docs/accelerate). If you want to learn more about how to train on a large number of GPUs, checkout this awesome guide: [Ultrascale Playbook](https://huggingface.co/spaces/nanotron/ultrascale-playbook).
For background on the underlying machinery, see the [Accelerate FSDP guide](https://huggingface.co/docs/accelerate/usage_guides/fsdp). To go deeper on large-scale training, check out the [Ultrascale Playbook](https://huggingface.co/spaces/nanotron/ultrascale-playbook).
+1 -1
View File
@@ -187,7 +187,7 @@ We use different IK initial guesses in the kinematic steps. As initial guess eit
- EEBoundsAndSafety: clamps the EE pose to a workspace and ratelimits jumps for safety. Also declares `action.ee.*` features.
- InverseKinematicsEEToJoints: turns an EE pose into joint positions with IK. `initial_guess_current_joints=True` is recommended for closedloop control; set `False` for openloop replay for stability.
- GripperVelocityToJoint: integrates a velocitylike gripper input into an absolute gripper position using the current measured state.
- ForwardKinematicsJointsToEEObservation: computes `observation.state.ee.*` from observed joints for logging and training on EE state.
- ForwardKinematicsJointsToEE: computes `observation.state.ee.*` from observed joints for logging and training on EE state.
### Troubleshooting
+1 -1
View File
@@ -58,7 +58,7 @@ robot_ee_to_joints_processor = RobotProcessorPipeline[RobotAction, RobotAction](
robot_joints_to_ee_pose = RobotProcessorPipeline[RobotObservation, RobotObservation]( # robot obs -> dataset obs
steps=[
ForwardKinematicsJointsToEEObservation(kinematics=kinematics_solver, motor_names=list(robot.bus.motors.keys()))
ForwardKinematicsJointsToEE(kinematics=kinematics_solver, motor_names=list(robot.bus.motors.keys()))
],
to_transition=observation_to_transition,
to_output=transition_to_observation,
+12
View File
@@ -40,3 +40,15 @@ lerobot-eval \
```
However, in most cases, presence of an accelerator is detected automatically and `policy.device` parameter can be omitted from CLI commands.
## Mixed precision
Training precision is owned by `--accelerator.mixed_precision`, which accepts `no` (default) and `bf16`:
```bash
lerobot-train \
--policy.type=act \
--accelerator.mixed_precision=bf16 ...
```
`bf16` requires an accelerator that supports it.
+287
View File
@@ -0,0 +1,287 @@
# Writing docstrings
LeRobot's API reference is generated directly from the docstrings in `src/lerobot/`. A docstring is not a
comment — it is the published documentation for that object, and the format below is what the renderer and
the CI checks parse.
This page is the contract. If you are adding or editing anything public in `src/lerobot/`, follow it.
> [!IMPORTANT]
> **An undocumented public method is an invisible one.** `[[autodoc]]` silently skips members that have no
> docstring — no warning, no error, it simply does not appear on the rendered page. Coverage and
> API-reference completeness are the same problem.
## The format in one example
Google section headers, Hugging Face type formatting. Both, not one or the other.
````python
def send_action(self, action: RobotAction, rate_hz: float = 30.0) -> RobotAction:
"""Command the robot to move to a target joint configuration.
Values are clipped by the configured maximum relative target before reaching the motors, so the
returned action may differ from the requested one.
Args:
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`):
Control loop frequency.
Returns:
`dict[str, float]`: The action actually written to the motors after safety clipping.
Raises:
DeviceNotConnectedError: If the robot has not been connected.
Example:
```python
>>> from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
>>> robot = SO101Follower(SO101FollowerConfig(port="/dev/ttyACM0")) # doctest: +SKIP
>>> robot.connect() # doctest: +SKIP
>>> robot.send_action({"shoulder_pan.pos": 0.0}) # doctest: +SKIP
```
"""
````
Cross-references are omitted from the examples on this page — see [Cross-references](#cross-references) for
their syntax and why they cannot be shown inside a code block.
## Rules
### Sections
`Args:` · `Returns:` · `Raises:` · `Yields:` · `Example:` · `Note:`
In that order. No other section headers. A one-line summary comes first, then an optional free-form
description, then the sections.
### The `Args:` line is machine-parsed
```
name (`type`, *optional*, defaults to `X`):
Description, indented on its own line.
```
The `*optional*, defaults to` clause is **checked against the real signature default** by
`make check-docstrings`. It is not decorative — if you write a default that has drifted from the code, CI
fails. Omit the clause entirely for required parameters:
```python
Args:
port (`str`):
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
max_relative_target (`float | dict[str, float]`, *optional*):
Caps the magnitude of the relative positional target vector. `None` disables clipping.
use_degrees (`bool`, *optional*, defaults to `True`):
Keep `True` for backward compatibility with existing policies and datasets.
```
Types go in backticks. Use `*optional*` with no `defaults to` when the default is `None` or is otherwise not
worth restating.
### `Returns:` is type-first
One indented line, type first, then a colon, then the description:
```python
Returns:
`dict[str, float]`: The action actually written to the motors after safety clipping.
```
`Yields:` takes the same shape.
### `**Attributes**:`, never `Attributes:`
doc-builder parses a bare `Attributes:` as a **synonym for `Parameters:`**, so your attributes get rendered
as constructor arguments. This is silent and wrong. Whenever the attributes differ from the constructor
parameters, use the bold form with a `--` separator:
```python
class Robot(abc.ABC):
"""The base abstract class for all LeRobot-compatible robots.
**Attributes**:
- **config_class** (`type[RobotConfig]`) -- The expected configuration class for this robot.
- **name** (`str`) -- The unique robot name used to identify this robot type.
"""
```
Note `--`, not `:`.
### Cross-references
Use doc-builder's bracket syntax: a square-bracketed backtick-quoted path. **Sphinx roles (`:pymeth:`,
`:pyattr:`) are not supported** and render as literal text on the page.
| Want | Write |
| ---------------------------- | ----------------------------------- |
| Class in the main package | &#91;`Robot`&#93; |
| Method, show the full path | &#91;`Robot.connect`&#93; |
| Method, show the bare name | &#91;`~Robot.connect`&#93; |
| Nested path | &#91;`~robots.Robot.connect`&#93; |
| Object in another HF library | &#91;`~accelerate.Accelerator`&#93; |
The `~` strips the path from the **link text only**; the link still resolves to the full path.
> [!NOTE]
> doc-builder resolves this syntax everywhere in a page — including inside fenced code blocks. That is why
> the docstring examples on this page use plain prose instead of cross-references: a code block containing
> one would render the resolved link rather than the syntax you need to type. In your own docstrings, use
> cross-references freely; this restriction only affects documentation _about_ the syntax.
### Callouts
Use GitHub-style blockquotes:
```markdown
> [!TIP]
> Call this once at startup — it takes about two seconds.
> [!WARNING]
> Torque is disabled on disconnect. The arm will drop if it is holding a load.
```
The `<Tip>` component is legacy per doc-builder; don't add new ones.
### Examples must be fenced
An example lives inside a fenced ` ```python ` block containing `>>> `. The fence is what makes it render
as a code block, and it is what the doctest preprocessor's regex looks for:
````python
Example:
```python
>>> from lerobot.robots.so_follower import SO101FollowerConfig
>>> cfg = SO101FollowerConfig(port="/dev/ttyACM0")
>>> cfg.use_degrees
True
```
````
> [!WARNING]
> An unfenced `>>>` is still collected — doctest finds prompts anywhere in a docstring. What you lose is the
> rendering, so it shows up as a wall of prose on the page. Every example needs the fence.
Every example either executes in CI or carries `# doctest: +SKIP`. Anything that touches hardware, a GPU, or
downloads from the Hub gets `+SKIP`:
````python
Example:
```python
>>> robot.connect() # doctest: +SKIP
>>> policy = ACTPolicy.from_pretrained("lerobot/act_aloha_sim_transfer_cube_human") # doctest: +SKIP
```
````
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
### Config dataclasses
Configuration fields are historically documented with `#` comments above each field. **doc-builder cannot
see inline comments** — such a class renders with every field listed and not a single description. Move them
into an `Args:` block on the class docstring:
```python
@dataclass
class SOFollowerConfig:
"""Configuration for SO-family follower arms.
Args:
port (`str`):
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
max_relative_target (`float | dict[str, float]`, *optional*):
Caps the magnitude of the relative positional target vector. A scalar applies to all motors;
a dict maps motor name to a per-motor cap. `None` disables clipping.
use_degrees (`bool`, *optional*, defaults to `True`):
Keep `True` for backward compatibility with existing policies and datasets.
"""
port: str
max_relative_target: float | dict[str, float] | None = None
use_degrees: bool = True
```
> [!IMPORTANT]
> **doc-builder does not inherit docstrings from base classes.** LeRobot's registered config classes are
> often thin multiple-inheritance shims:
>
> ```python
> @RobotConfig.register_subclass("so101_follower")
> @dataclass
> class SOFollowerRobotConfig(RobotConfig, SOFollowerConfig):
> pass
> ```
>
> That class renders **every** field — including the ones it inherits — with no descriptions at all, no
> matter how well the bases are documented. The `Args:` block must live on the concrete class that
> `[[autodoc]]` names, and it must cover inherited fields too.
### Base class, then concrete subclass
The abstract base carries the canonical contract. Subclasses document only what deviates — port semantics,
calibration quirks, motor layout, supported feature keys. Do not copy the base contract into every subclass.
`Robot`, `Teleoperator`, `Camera`, `MotorsBus`, `ProcessorStep`, and `PreTrainedPolicy` all follow this
shape.
### Module-level aliases
Several public names are aliases rather than distinct classes:
```python
SO100FollowerConfig = SOFollowerRobotConfig
SO101FollowerConfig = SOFollowerRobotConfig
```
`[[autodoc]]` resolves the alias and renders the **canonical** class name, so a `## SO101FollowerConfig`
heading will show `class lerobot.robots.so_follower.SOFollowerRobotConfig` in the body. Document the
canonical class once, and mention the aliases in the page's prose rather than giving each alias its own
autodoc block.
## What not to document
- **Private members.** Anything starting with `_` is not part of the public API.
- **The type annotation restated as prose.** `port (`str`): A string.` adds nothing. Say what it is for.
- **Vendored upstream code.** `src/lerobot/policies/molmoact2/molmoact2_hf_model/` is vendored from
`transformers` and already carries upstream-style docstrings. Leave it alone — restyling it only creates
conflicts on the next sync. It is excluded from the API reference and from the docstring checks.
## How this is enforced
| Check | What it catches |
| ------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `make check-docstrings` | An `Args:` entry that doesn't match the signature; a documented default that has drifted from the real one |
| `make doctest` | Examples that no longer run |
| `make check-doctest-list` | Stale or unsorted entries in `utils/documentation_tests.txt` |
| `ruff` (`D` rules) | Google-convention style violations |
| `interrogate` | Docstring coverage falling below the current threshold |
| doc-builder | A `[[autodoc]]` path that points at something that doesn't exist — this breaks the docs build |
Run them together before opening a PR:
```bash
make check-docstrings && make doctest && pre-commit run --all-files
```
Then render the page and actually look at it:
```bash
doc-builder build lerobot docs/source/ --build_dir /tmp/doc-build
```
## Checklist
- [ ] Every public member you touched has a docstring.
- [ ] Every `Args:` entry matches the signature, including the `*optional*, defaults to` clause.
- [ ] `Returns:` is type-first on one indented line.
- [ ] No bare `Attributes:` — use `**Attributes**:` with `--` separators.
- [ ] No Sphinx roles — cross-references use &#91;`~module.Class.method`&#93;.
- [ ] Examples are inside a fenced ` ```python ` block, and either run in CI or carry `# doctest: +SKIP`.
- [ ] Config dataclass fields are in an `Args:` block on the concrete class, not `#` comments.
- [ ] The rendered page has been eyeballed.
+2 -2
View File
@@ -36,7 +36,7 @@ from lerobot.processor import (
)
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.utils.constants import ACTION, OBS_STR
@@ -95,7 +95,7 @@ def main():
# Build pipeline to convert joints observation to EE observation
robot_joints_to_ee_pose_processor = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[
ForwardKinematicsJointsToEEObservation(
ForwardKinematicsJointsToEE(
kinematics=kinematics_solver, motor_names=list(robot.bus.motors.keys())
)
],
+2 -2
View File
@@ -29,7 +29,7 @@ from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
EEBoundsAndSafety,
EEReferenceAndDelta,
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
GripperVelocityToJoint,
InverseKinematicsEEToJoints,
)
@@ -111,7 +111,7 @@ def main():
# Build pipeline to convert joint observation to EE observation (FK).
robot_joints_to_ee_pose = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[
ForwardKinematicsJointsToEEObservation(
ForwardKinematicsJointsToEE(
kinematics=kinematics_solver, motor_names=list(robot.bus.motors.keys())
)
],
+2 -2
View File
@@ -38,7 +38,7 @@ from lerobot.processor import (
)
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
@@ -75,7 +75,7 @@ def main():
)
robot_joints_to_ee_pose_processor = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[ForwardKinematicsJointsToEEObservation(kinematics=kinematics_solver, motor_names=motor_names)],
steps=[ForwardKinematicsJointsToEE(kinematics=kinematics_solver, motor_names=motor_names)],
to_transition=observation_to_transition,
to_output=transition_to_observation,
)
+2 -2
View File
@@ -36,7 +36,7 @@ from lerobot.processor import (
)
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.utils.constants import ACTION, OBS_STR
@@ -95,7 +95,7 @@ def main():
# Build pipeline to convert joints observation to EE observation
robot_joints_to_ee_pose_processor = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[
ForwardKinematicsJointsToEEObservation(
ForwardKinematicsJointsToEE(
kinematics=kinematics_solver, motor_names=list(robot.bus.motors.keys())
)
],
+3 -4
View File
@@ -29,8 +29,7 @@ from lerobot.processor import (
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
EEBoundsAndSafety,
ForwardKinematicsJointsToEEAction,
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.scripts.lerobot_record import record_loop
@@ -79,7 +78,7 @@ def main():
# Build pipeline to convert follower joints to EE observation.
follower_joints_to_ee = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[
ForwardKinematicsJointsToEEObservation(
ForwardKinematicsJointsToEE(
kinematics=follower_kinematics_solver, motor_names=list(follower.bus.motors.keys())
),
],
@@ -90,7 +89,7 @@ def main():
# Build pipeline to convert leader joints to EE action.
leader_joints_to_ee = RobotProcessorPipeline[tuple[RobotAction, RobotObservation], RobotAction](
steps=[
ForwardKinematicsJointsToEEAction(
ForwardKinematicsJointsToEE(
kinematics=leader_kinematics_solver, motor_names=list(leader.bus.motors.keys())
),
],
+2 -2
View File
@@ -36,7 +36,7 @@ from lerobot.processor import (
)
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
ForwardKinematicsJointsToEEObservation,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
@@ -78,7 +78,7 @@ def main():
# Joint-space observation → EE-space observation (consumed by the policy).
robot_joints_to_ee_pose_processor = RobotProcessorPipeline[RobotObservation, RobotObservation](
steps=[ForwardKinematicsJointsToEEObservation(kinematics=kinematics_solver, motor_names=motor_names)],
steps=[ForwardKinematicsJointsToEE(kinematics=kinematics_solver, motor_names=motor_names)],
to_transition=observation_to_transition,
to_output=transition_to_observation,
)
+2 -2
View File
@@ -27,7 +27,7 @@ from lerobot.processor import (
from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
from lerobot.robots.so_follower.robot_kinematic_processor import (
EEBoundsAndSafety,
ForwardKinematicsJointsToEEAction,
ForwardKinematicsJointsToEE,
InverseKinematicsEEToJoints,
)
from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig
@@ -65,7 +65,7 @@ def main():
# Build pipeline to convert teleop joints to EE action
leader_to_ee = RobotProcessorPipeline[RobotAction, RobotAction](
steps=[
ForwardKinematicsJointsToEEAction(
ForwardKinematicsJointsToEE(
kinematics=leader_kinematics_solver, motor_names=list(leader.bus.motors.keys())
),
],
+118 -17
View File
@@ -346,6 +346,7 @@ lerobot-record="lerobot.scripts.lerobot_record:main"
lerobot-replay="lerobot.scripts.lerobot_replay:main"
lerobot-setup-motors="lerobot.scripts.lerobot_setup_motors:main"
lerobot-teleoperate="lerobot.scripts.lerobot_teleoperate:main"
lerobot-convert-dcp="lerobot.scripts.lerobot_convert_dcp:main"
lerobot-eval="lerobot.scripts.lerobot_eval:main"
lerobot-train="lerobot.scripts.lerobot_train:main"
lerobot-train-tokenizer="lerobot.scripts.lerobot_train_tokenizer:main"
@@ -400,19 +401,101 @@ exclude = ["tests/artifacts/**/*.safetensors", "*_pb2.py", "*_pb2_grpc.py"]
# N: pep8-naming
# TODO: Uncomment rules when ready to use
select = [
"E", "W", "F", "I", "B", "C4", "T20", "N", "UP", "SIM" #, "A", "S", "D", "RUF"
"E", "W", "F", "I", "B", "C4", "T20", "N", "UP", "SIM", "D" #, "A", "S", "RUF"
]
ignore = [
"E501", # Line too long
"T201", # Print statement found
"T203", # Pprint statement found
"B008", # Perform function call in argument defaults
# D100/D104: module- and package-level docstrings. The API reference is generated from class and
# function docstrings; a banner at the top of every file and every __init__.py would not appear on any
# rendered page. Coverage of the things that do get rendered is enforced by interrogate instead.
"D100",
"D104",
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "F403", "E402"]
"__init__.py" = ["F401", "F403", "E402", "D104"]
# E402: conditional-import guards (TYPE_CHECKING / is_package_available) must precede the imports they protect
"src/lerobot/scripts/convert_dataset_v21_to_v30.py" = ["E402"]
# D (pydocstyle) is enabled globally, but only holds for code that has been converted to the docstring
# standard in docs/source/writing_docstrings.mdx. Every module below is still on the old style; each entry
# is deleted as that module is converted, and this block can be removed once it is empty.
#
# Not part of the API reference and not planned for conversion: tests, examples, benchmarks, templates,
# CI helper scripts and the packaging shim.
"tests/**" = ["D"]
"examples/**" = ["D"]
"benchmarks/**" = ["D"]
"scripts/**" = ["D"]
"setup.py" = ["D"]
"src/lerobot/templates/**" = ["D"]
# Vendored from transformers; keeps its upstream docstring style so syncs stay clean.
"src/lerobot/policies/molmoact2/molmoact2_hf_model/**" = ["D"]
# Awaiting conversion, one PR per module.
"src/lerobot/annotations/**" = ["D"]
"src/lerobot/async_inference/**" = ["D"]
"src/lerobot/cameras/**" = ["D"]
"src/lerobot/common/**" = ["D"]
"src/lerobot/configs/**" = ["D"]
"src/lerobot/data_processing/**" = ["D"]
"src/lerobot/datasets/**" = ["D"]
"src/lerobot/distributed/**" = ["D"]
"src/lerobot/envs/**" = ["D"]
"src/lerobot/jobs/**" = ["D"]
"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/processor/**" = ["D"]
"src/lerobot/rewards/**" = ["D"]
"src/lerobot/rl/**" = ["D"]
"src/lerobot/rollout/**" = ["D"]
"src/lerobot/scripts/**" = ["D"]
"src/lerobot/teleoperators/**" = ["D"]
"src/lerobot/transforms/**" = ["D"]
"src/lerobot/transport/**" = ["D"]
"src/lerobot/utils/**" = ["D"]
"src/lerobot/lerobot_types.py" = ["D"]
[tool.ruff.lint.isort]
combine-as-imports = true
known-first-party = ["lerobot"]
@@ -456,25 +539,34 @@ default.extend-ignore-identifiers-re = [
"seperated_timestep",
]
# TODO: Uncomment when ready to use
# [tool.interrogate]
# ignore-init-module = true
# ignore-init-method = true
# ignore-nested-functions = false
# ignore-magic = false
# ignore-semiprivate = false
# ignore-private = false
# ignore-property-decorators = false
# ignore-module = false
# ignore-setters = false
# fail-under = 80
# output-format = "term-missing"
# color = true
# paths = ["src/lerobot"]
# Docstring coverage gate. `fail-under` is a RATCHET, not a target: it is set just below the currently
# measured coverage so it passes today, and is raised in the same PR that documents a module. Never set it
# to a value that fails on main. The destination is 100; see docs/source/writing_docstrings.mdx.
[tool.interrogate]
ignore-init-module = true
ignore-init-method = true
ignore-nested-functions = false
ignore-magic = false
ignore-semiprivate = false
ignore-private = false
ignore-property-decorators = false
ignore-module = false
ignore-setters = false
fail-under = 58
output-format = "term-missing"
color = true
paths = ["src/lerobot"]
exclude = ["src/lerobot/policies/molmoact2/molmoact2_hf_model"]
# TODO: Enable mypy gradually module by module across multiple PRs
# Uncomment [tool.mypy] first, then uncomment individual module overrides as they get proper type annotations
[tool.pytest.ini_options]
markers = [
"multigpu: distributed tests needing 2-4 GPUs (CI: docker_publish.yml lane)",
"multigpu_heavy: 8-GPU sweeps and soak tests; never run in CI",
]
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
@@ -521,6 +613,15 @@ disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
[[tool.mypy.overrides]]
module = "lerobot.distributed.*"
ignore_errors = false
# extra strictness for the distributed engine
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
[[tool.mypy.overrides]]
module = "lerobot.optim.*"
ignore_errors = false
+1 -2
View File
@@ -14,8 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""
LeRobot -- PyTorch library for real-world robotics.
"""LeRobot -- PyTorch library for real-world robotics.
Provides datasets, pretrained policies, and tools for training, evaluation,
data collection, and robot control. Integrates with Hugging Face Hub for
+1 -1
View File
@@ -13,7 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""To enable `lerobot.__version__`"""
"""To enable `lerobot.__version__`."""
from importlib.metadata import PackageNotFoundError, version
+4 -4
View File
@@ -33,10 +33,10 @@ class Camera(abc.ABC):
- Connection/disconnection
- Frame capture (sync/async/latest)
Attributes:
fps (int | None): Configured frames per second
width (int | None): Frame width in pixels
height (int | None): Frame height in pixels
**Attributes**:
- **fps** (`int | None`) -- Configured frames per second.
- **width** (`int | None`) -- Frame width in pixels.
- **height** (`int | None`) -- Frame height in pixels.
"""
def __init__(self, config: CameraConfig):
@@ -40,17 +40,20 @@ class OpenCVCameraConfig(CameraConfig):
OpenCVCameraConfig(0, 30, 1280, 720, fourcc="YUYV") # With YUYV format
```
Attributes:
index_or_path: Either an integer representing the camera device index,
or a Path object pointing to a video file.
fps: Requested frames per second for the color stream.
width: Requested frame width in pixels for the color stream.
height: Requested frame height in pixels for the color stream.
color_mode: Color mode for image output (RGB or BGR). Defaults to RGB.
rotation: Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no rotation.
warmup_s: Time reading frames before returning from connect (in seconds)
fourcc: FOURCC code for video format (e.g., "MJPG", "YUYV", "I420"). Defaults to None (auto-detect).
backend: OpenCV backend identifier (https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html). Defaults to ANY.
**Attributes**:
- **index_or_path** (`int | Path`) -- Either an integer representing the camera device index, or a
Path object pointing to a video file.
- **fps** -- Requested frames per second for the color stream.
- **width** -- Requested frame width in pixels for the color stream.
- **height** -- Requested frame height in pixels for the color stream.
- **color_mode** (`ColorMode`) -- Color mode for image output (RGB or BGR). Defaults to RGB.
- **rotation** (`Cv2Rotation`) -- Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no
rotation.
- **warmup_s** (`int`) -- Time reading frames before returning from connect (in seconds)
- **fourcc** (`str | None`) -- FOURCC code for video format (e.g., "MJPG", "YUYV", "I420"). Defaults
to None (auto-detect).
- **backend** (`Cv2Backends`) -- OpenCV backend identifier
(https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html). Defaults to ANY.
Note:
- Only 3-channel color output (RGB/BGR) is currently supported.
@@ -43,16 +43,16 @@ class Reachy2CameraConfig(CameraConfig):
) # Left teleop camera, 640x480 @ 30FPS
```
Attributes:
name: Name of the camera device. Can be "teleop" or "depth".
image_type: Type of image stream. For "teleop" camera, can be "left" or "right".
For "depth" camera, can be "rgb" or "depth". (depth is not supported yet)
fps: Requested frames per second for the color stream. Not configurable for Reachy 2 cameras.
width: Requested frame width in pixels for the color stream.
height: Requested frame height in pixels for the color stream.
color_mode: Color mode for image output (RGB or BGR). Defaults to RGB.
ip_address: IP address of the robot. Defaults to "localhost".
port: Port number for the camera server. Defaults to 50065.
**Attributes**:
- **name** (`str`) -- Name of the camera device. Can be "teleop" or "depth".
- **image_type** (`str`) -- Type of image stream. For "teleop" camera, can be "left" or "right". For
"depth" camera, can be "rgb" or "depth". (depth is not supported yet)
- **fps** -- Requested frames per second for the color stream. Not configurable for Reachy 2 cameras.
- **width** -- Requested frame width in pixels for the color stream.
- **height** -- Requested frame height in pixels for the color stream.
- **color_mode** (`ColorMode`) -- Color mode for image output (RGB or BGR). Defaults to RGB.
- **ip_address** (`str | None`) -- IP address of the robot. Defaults to "localhost".
- **port** (`int`) -- Port number for the camera server. Defaults to 50065.
Note:
- Only 3-channel color output (RGB/BGR) is currently supported.
@@ -36,27 +36,28 @@ class RealSenseCameraConfig(CameraConfig):
RealSenseCameraConfig("0123456789", 30, 640, 480, rotation=Cv2Rotation.ROTATE_90) # With 90° rotation
```
Attributes:
fps: Requested frames per second for the color stream.
width: Requested frame width in pixels for the color stream.
height: Requested frame height in pixels for the color stream.
serial_number_or_name: Unique serial number or human-readable name to identify the camera.
color_mode: Color mode for image output (RGB or BGR). Defaults to RGB.
use_rgb: Whether to enable the color stream. Defaults to True.
use_depth: Whether to enable depth stream. Defaults to False.
rotation: Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no rotation.
warmup_s: Time reading frames before returning from connect (in seconds)
exposure: Manual exposure value for the color sensor. When set, auto-exposure is
disabled and this fixed value is used. Valid ranges are camera-model specific
and reported if the value is rejected. Defaults to None (leave unchanged).
gain: Manual gain value for the color sensor. When set, auto-exposure is disabled
and this fixed gain is used, which also freezes exposure at its current value
when no exposure is configured. Valid ranges are camera-model specific and
reported if the value is rejected. Defaults to None (leave unchanged).
white_balance: Manual white balance value for the color sensor. When set, auto
white balance is disabled and this fixed value is used. Valid ranges are
camera-model specific and reported if the value is rejected. Defaults to None
(leave unchanged).
**Attributes**:
- **fps** -- Requested frames per second for the color stream.
- **width** -- Requested frame width in pixels for the color stream.
- **height** -- Requested frame height in pixels for the color stream.
- **serial_number_or_name** (`str`) -- Unique serial number or human-readable name to identify the
camera.
- **color_mode** (`ColorMode`) -- Color mode for image output (RGB or BGR). Defaults to RGB.
- **use_rgb** (`bool`) -- Whether to enable the color stream. Defaults to True.
- **use_depth** (`bool`) -- Whether to enable depth stream. Defaults to False.
- **rotation** (`Cv2Rotation`) -- Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no
rotation.
- **warmup_s** (`int`) -- Time reading frames before returning from connect (in seconds)
- **exposure** (`int | None`) -- Manual exposure value for the color sensor. When set, auto-exposure
is disabled and this fixed value is used. Valid ranges are camera-model specific and reported if the
value is rejected. Defaults to None (leave unchanged).
- **gain** (`int | None`) -- Manual gain value for the color sensor. When set, auto-exposure is
disabled and this fixed gain is used, which also freezes exposure at its current value when no
exposure is configured. Valid ranges are camera-model specific and reported if the value is
rejected. Defaults to None (leave unchanged).
- **white_balance** (`int | None`) -- Manual white balance value for the color sensor. When set, auto
white balance is disabled and this fixed value is used. Valid ranges are camera-model specific and
reported if the value is rejected. Defaults to None (leave unchanged).
Note:
- Either name or serial_number must be specified.
+604 -174
View File
@@ -13,16 +13,41 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pathlib import Path
"""Training-output persistence: checkpoints, two-phase resume, and hub publishing.
from huggingface_hub import HfApi, snapshot_download
Rank discipline: every function here that can
contain a collective is documented as such and must run on ALL ranks; rank-0-only file writes
sit under one grouped ``is_main_process()`` gate per contiguous region, placed below all
collectives. The leaf save/load helpers carry no rank gates of their own — the exception is
``PreTrainedPolicy._save_pretrained``, whose gate is internal because its collective gather and
its writes live in the same method.
"""
import logging
from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Any
import torch.distributed as dist
from huggingface_hub import HfApi, ModelCard, ModelCardData, snapshot_download
from torch.optim import Optimizer
from torch.optim.lr_scheduler import LRScheduler
from lerobot.__version__ import __version__
from lerobot.configs.policies import PreTrainedConfig
from lerobot.configs.rewards import RewardModelConfig
from lerobot.configs.train import TrainPipelineConfig
from lerobot.distributed.checkpoint import (
is_sharded_module,
load_sharded_model,
load_sharded_optimizer,
save_sharded_model,
save_sharded_optimizer,
)
from lerobot.distributed.utils import is_main_process
from lerobot.optim import (
load_optimizer_state,
load_optimizer_state_dict,
load_scheduler_state,
save_optimizer_state,
save_scheduler_state,
@@ -40,14 +65,39 @@ from lerobot.utils.hub import find_latest_hub_checkpoint
from lerobot.utils.io_utils import load_json, write_json
from lerobot.utils.random_utils import load_rng_state, save_rng_state
if TYPE_CHECKING:
from accelerate import Accelerator
from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata
from lerobot.rewards.pretrained import PreTrainedRewardModel
def get_step_identifier(step: int, total_steps: int) -> str:
"""Format a step number as the zero-padded identifier used for checkpoint directory names.
Args:
step (int): The training step to format.
total_steps (int): The total number of training steps; sets the padding width
(minimum 6 digits).
Returns:
str: The zero-padded step identifier, e.g. `"005000"`.
"""
num_digits = max(6, len(str(total_steps)))
return f"{step:0{num_digits}d}"
def get_step_checkpoint_dir(output_dir: Path, total_steps: int, step: int) -> Path:
"""Returns the checkpoint sub-directory corresponding to the step number."""
"""Returns the checkpoint sub-directory corresponding to the step number.
Args:
output_dir (Path): The training run's output directory.
total_steps (int): The total number of training steps; sets the identifier padding.
step (int): The training step of the checkpoint.
Returns:
Path: The checkpoint step directory, `output_dir/checkpoints/<step-identifier>`.
"""
step_identifier = get_step_identifier(step, total_steps)
return output_dir / CHECKPOINTS_DIR / step_identifier
@@ -63,37 +113,15 @@ def should_save_checkpoint(step: int, save_freq: int, total_steps: int) -> bool:
return (save_freq > 0 and step % save_freq == 0) or step == total_steps
def save_training_step(
step: int, save_dir: Path, num_processes: int | None = None, batch_size: int | None = None
) -> None:
state: dict = {"step": step}
# num_processes and batch_size are recorded so a resumed run can detect a changed world size or
# batch size: the sampler's resume offset is computed from the (num_processes, batch_size) that
# produced `step`, since both scale how many sampler positions a step consumes (see
# compute_sampler_state).
if num_processes is not None:
state["num_processes"] = num_processes
if batch_size is not None:
state["batch_size"] = batch_size
write_json(state, save_dir / TRAINING_STEP)
def update_last_checkpoint(checkpoint_dir: Path) -> None:
"""Point the `last` symlink in the checkpoints directory at the given checkpoint.
Any existing `last` symlink is replaced. The link target is relative to the checkpoints
directory, so the tree stays valid when the run directory is moved.
def load_training_step(save_dir: Path) -> int:
training_step = load_json(save_dir / TRAINING_STEP)
return training_step["step"]
def load_training_num_processes(checkpoint_dir: Path) -> int | None:
"""World size recorded at checkpoint time, or None for checkpoints written before it was stored."""
return load_json(checkpoint_dir / TRAINING_STATE_DIR / TRAINING_STEP).get("num_processes")
def load_training_batch_size(checkpoint_dir: Path) -> int | None:
"""Per-process batch size recorded at checkpoint time, or None for older checkpoints."""
return load_json(checkpoint_dir / TRAINING_STATE_DIR / TRAINING_STEP).get("batch_size")
def update_last_checkpoint(checkpoint_dir: Path) -> Path:
Args:
checkpoint_dir (Path): The checkpoint step directory the `last` link should target.
"""
last_checkpoint_dir = checkpoint_dir.parent / LAST_CHECKPOINT_LINK
if last_checkpoint_dir.is_symlink():
last_checkpoint_dir.unlink()
@@ -101,6 +129,68 @@ def update_last_checkpoint(checkpoint_dir: Path) -> Path:
last_checkpoint_dir.symlink_to(relative_target)
# ---------------------------------------------------------------------------------------------
# training_step.json
# ---------------------------------------------------------------------------------------------
def save_training_metadata(step: int, save_dir: Path, cfg: TrainPipelineConfig) -> None:
"""Record the step counter plus everything a resume needs to reason about topology changes.
`step` counts loop iterations (= micro-batches), so
the sampler resume offset is `step x batch_size x dp_world_size` with no grad-accum factor.
`grad_accum_steps` and the parallelism snapshot are recorded so a resume can warn precisely
when the optimizer-update cadence or the sharding topology changed.
Args:
step (int): The training step (micro-batch counter) to record.
save_dir (Path): The `training_state/` directory to write `training_step.json` into.
cfg (TrainPipelineConfig): The training config whose batch size, gradient-accumulation,
and parallelism settings are snapshotted alongside the step.
"""
state: dict[str, Any] = {
"step": step,
"dp_world_size": cfg.parallelism.dp_world_size,
"batch_size": cfg.batch_size,
"grad_accum_steps": cfg.accelerator.gradient_accumulation.steps,
"parallelism": {
"dp_replicate": cfg.parallelism.dp_replicate,
"dp_shard": cfg.parallelism.dp_shard,
"ring_degree": cfg.parallelism.context_parallel.ring_degree,
"ulysses_degree": cfg.parallelism.context_parallel.ulysses_degree,
},
}
write_json(state, save_dir / TRAINING_STEP)
def load_training_metadata(training_state_dir: Path) -> dict[str, Any]:
"""Read everything `save_training_metadata` recorded, in a single pass.
Every key is always present: fields a checkpoint predates come back as None, so a caller
reading `metadata["batch_size"]` gets a KeyError on a typo rather than a silent None.
Args:
training_state_dir (Path): The checkpoint's `training_state/` directory.
Returns:
dict[str, Any]: `step` plus the `dp_world_size`, `batch_size`, `grad_accum_steps` and
`parallelism` snapshot recorded alongside it (None where not recorded).
"""
state = load_json(training_state_dir / TRAINING_STEP)
return {
"step": int(state["step"]),
"dp_world_size": state.get("dp_world_size", state.get("num_processes")),
"batch_size": state.get("batch_size"),
"grad_accum_steps": state.get("grad_accum_steps"),
"parallelism": state.get("parallelism"),
}
# ---------------------------------------------------------------------------------------------
# Checkpoint save
# ---------------------------------------------------------------------------------------------
def save_checkpoint(
checkpoint_dir: Path,
step: int,
@@ -110,192 +200,301 @@ def save_checkpoint(
scheduler: LRScheduler | None = None,
preprocessor: PolicyProcessorPipeline | None = None,
postprocessor: PolicyProcessorPipeline | None = None,
num_processes: int | None = None,
batch_size: int | None = None,
model_state_dict: dict | None = None,
optim_state_dict: dict | None = None,
accelerator: "Accelerator | None" = None,
) -> None:
"""This function creates the following directory structure:
005000/ # training step at checkpoint
├── pretrained_model/
│ ├── config.json # policy config
│ ├── model.safetensors # policy weights
│ ├── model.safetensors # policy weights (checkpoint_format ∈ {safetensors, safetensors_dcp}, or any non-sharded run)
│ ├── pytorch_model_fsdp_0/ # DCP model shards (checkpoint_format ∈ {dcp, safetensors_dcp})
│ ├── train_config.json # train config
│ ├── processor.json # processor config (if preprocessor provided)
── step_*.safetensors # processor state files (if any)
│ ├── policy_preprocessor.json # preprocessor config (if preprocessor provided)
── policy_preprocessor_step_*.safetensors # state of the stateful preprocessor steps
│ ├── policy_postprocessor.json # postprocessor config (if postprocessor provided)
│ └── policy_postprocessor_step_*.safetensors # state of the stateful postprocessor steps
└── training_state/
├── optimizer_param_groups.json # optimizer param groups
├── optimizer_state.safetensors # optimizer state
├── optimizer_param_groups.json # optimizer param groups (non-sharded runs)
├── optimizer_state.safetensors # optimizer state (non-sharded runs)
├── optimizer_0/ # DCP optimizer shards (sharded runs)
├── rng_state.safetensors # rng states
├── scheduler_state.json # scheduler state
└── training_step.json # training step
├── scheduler_state.json # scheduler state (if scheduler provided)
└── training_step.json # training step + dp_world_size/batch_size/grad_accum + topology
Collective: MUST be called on every rank. Rank-0-only writes are gated internally, so the
call site needs no rank branches.
Args:
cfg (TrainPipelineConfig): The training config used for this run.
checkpoint_dir (Path): The checkpoint step directory to write (e.g. `.../checkpoints/005000`).
step (int): The training step at that checkpoint.
cfg (TrainPipelineConfig): The training config used for this run.
policy (PreTrainedPolicy): The policy to save.
optimizer (Optimizer | None, optional): The optimizer to save the state from. Defaults to None.
optimizer (Optimizer): The optimizer to save the state from.
scheduler (LRScheduler | None, optional): The scheduler to save the state from. Defaults to None.
preprocessor: The preprocessor/pipeline to save. Defaults to None.
postprocessor: The postprocessor/pipeline to save. Defaults to None.
num_processes (int | None, optional): Distributed world size to record for sample-exact
resume. Defaults to None (not recorded).
batch_size (int | None, optional): Per-process batch size to record for sample-exact
resume. Defaults to None (not recorded).
model_state_dict: Pre-gathered full (unsharded) model state dict. Required under FSDP,
where `policy.state_dict()` would return sharded tensors; the caller gathers it via a
cross-rank collective and passes it here so rank 0 can write it directly. It holds
FSDP's fp32 master weights and is saved as-is (the loader casts to the policy dtype on
read). When None (DDP / single-GPU), the model is saved the normal way. Defaults to None.
optim_state_dict: Pre-gathered full (unsharded) optimizer state dict. Required under FSDP
(gathered alongside `model_state_dict` via `gather_fsdp_state_dicts`); saved in the same
safetensors format as the single-GPU path. When None, `optimizer.state_dict()` is used.
preprocessor (PolicyProcessorPipeline | None, optional): The preprocessor/pipeline to save.
Defaults to None.
postprocessor (PolicyProcessorPipeline | None, optional): The postprocessor/pipeline to save.
Defaults to None.
accelerator (Accelerator | None, optional): The accelerator the policy was prepared with;
used to unwrap the model and required on sharded runs, where it owns the DCP save
channels. Defaults to None (plain single-process saves).
"""
pretrained_dir = checkpoint_dir / PRETRAINED_MODEL_DIR
policy.save_pretrained(pretrained_dir, state_dict=model_state_dict)
cfg.save_pretrained(pretrained_dir)
fmt = cfg.checkpoint_format
policy_to_save = accelerator.unwrap_model(policy) if accelerator is not None else policy
sharded = is_sharded_module(policy_to_save)
# -- model artifact(s): the two collective-capable calls ----------------------------------
if cfg.peft is not None:
# When using PEFT, policy.save_pretrained will only write the adapter weights + config, not the
# policy config which we need for loading the model. In this case we'll write it ourselves.
policy.config.save_pretrained(pretrained_dir)
if preprocessor is not None:
preprocessor.save_pretrained(pretrained_dir)
if postprocessor is not None:
postprocessor.save_pretrained(pretrained_dir)
# PeftModel.save_pretrained is an external API with no internal rank gate, and the
# adapters are replicated (PEFT x sharded is rejected at validation): main rank writes.
if is_main_process():
policy_to_save.save_pretrained(pretrained_dir)
elif fmt.wants_safetensors or not sharded:
# Collective when sharded (full gather); writes happen on the main process only in all
# multi-rank layouts (the gate lives inside _save_pretrained, next to its collective gather).
policy_to_save.save_pretrained(pretrained_dir)
if fmt.wants_dcp and sharded:
save_sharded_model(accelerator, policy_to_save, pretrained_dir)
# -- sidecar configs: ONE gate for the whole contiguous rank-0-only region ----------------
if is_main_process():
if fmt.wants_dcp and not fmt.wants_safetensors:
# save_pretrained did not run: keep the DCP-only checkpoint self-describing.
policy_to_save.config.save_pretrained(pretrained_dir)
cfg.save_pretrained(pretrained_dir)
if cfg.peft is not None:
# PEFT's save_pretrained writes only adapter weights + config; the policy config
# needed to reload the base model is written explicitly.
policy_to_save.config.save_pretrained(pretrained_dir)
if preprocessor is not None:
preprocessor.save_pretrained(pretrained_dir)
if postprocessor is not None:
postprocessor.save_pretrained(pretrained_dir)
save_training_state(
checkpoint_dir,
step,
optimizer,
scheduler,
num_processes=num_processes,
batch_size=batch_size,
optim_state_dict=optim_state_dict,
checkpoint_dir, step, cfg, optimizer, scheduler, accelerator, sharded=sharded, model=policy_to_save
)
if accelerator is not None:
accelerator.wait_for_everyone()
def save_training_state(
checkpoint_dir: Path,
train_step: int,
optimizer: Optimizer | None = None,
step: int,
cfg: TrainPipelineConfig,
optimizer: Optimizer | dict[str, Optimizer] | None = None,
scheduler: LRScheduler | None = None,
num_processes: int | None = None,
batch_size: int | None = None,
optim_state_dict: dict | None = None,
accelerator: "Accelerator | None" = None,
*,
sharded: bool = False,
model: PreTrainedPolicy | None = None,
) -> None:
"""
Saves the training step, optimizer state, scheduler state, and rng state.
"""Write training_state/. Collective under sharding: call on every rank.
Args:
save_dir (Path): The directory to save artifacts to.
train_step (int): Current training step.
optimizer (Optimizer | None, optional): The optimizer from which to save the state_dict.
checkpoint_dir (Path): The checkpoint step directory; `training_state/` is created inside it.
step (int): The training step at that checkpoint.
cfg (TrainPipelineConfig): The training config used for this run (its topology and
accumulation settings are recorded in `training_step.json`).
optimizer (Optimizer | dict[str, Optimizer] | None, optional): The optimizer(s) to save
the state from. Defaults to None.
scheduler (LRScheduler | None, optional): The scheduler to save the state from.
Defaults to None.
scheduler (LRScheduler | None, optional): The scheduler from which to save the state_dict.
Defaults to None.
num_processes (int | None, optional): Distributed world size to record. Defaults to None.
batch_size (int | None, optional): Per-process batch size to record. Defaults to None.
optim_state_dict: Pre-gathered full optimizer state dict (for FSDP). Saved instead of
`optimizer.state_dict()` when provided. Defaults to None.
accelerator (Accelerator | None, optional): Required when `sharded` is True — it owns
the DCP optimizer save channel. Defaults to None.
sharded (bool): The model's sharding state, computed once in `save_checkpoint` and
threaded here so the two sites cannot disagree. Defaults to False.
model (PreTrainedPolicy | None, optional): Required only for the sharded optimizer
channel: torch's optimizer DCP APIs are model-coupled (the state dict is keyed by
model FQNs), so accelerate's `save_fsdp_optimizer` needs the sharded module
alongside the optimizer. Defaults to None.
"""
save_dir = checkpoint_dir / TRAINING_STATE_DIR
# All ranks: the directory must exist before the DCP optimizer collective writes into it
# (exist_ok makes the concurrent mkdir race-free on shared filesystems).
save_dir.mkdir(parents=True, exist_ok=True)
save_training_step(train_step, save_dir, num_processes=num_processes, batch_size=batch_size)
save_rng_state(save_dir)
if optimizer is not None:
save_optimizer_state(optimizer, save_dir, optim_state_dict=optim_state_dict)
if scheduler is not None:
save_scheduler_state(scheduler, save_dir)
if optimizer is not None and sharded:
if accelerator is None or model is None:
raise ValueError("Saving a sharded optimizer state requires the accelerator and model.")
# Collective — all ranks write their DCP shards into optimizer_0/.
save_sharded_optimizer(accelerator, optimizer, model, save_dir)
if is_main_process(): # ONE grouped gate for the whole rank-0-only region
save_training_metadata(step, save_dir, cfg)
save_rng_state(save_dir)
if scheduler is not None:
save_scheduler_state(scheduler, save_dir)
if optimizer is not None and not sharded:
save_optimizer_state(optimizer, save_dir)
def load_training_state(
checkpoint_dir: Path, optimizer: Optimizer, scheduler: LRScheduler | None, load_optimizer: bool = True
) -> tuple[int, Optimizer, LRScheduler | None]:
"""
Loads the training step, optimizer state, scheduler state, and rng state.
This is used to resume a training run.
# ---------------------------------------------------------------------------------------------
# Two-phase resume
# ---------------------------------------------------------------------------------------------
def resume_before_prepare(cfg: TrainPipelineConfig) -> int:
"""Phase 1 — before `accelerator.prepare()`: restore RNG and return the step counter.
Pure loaders only. The sampler resume offset is *derived* from the returned step inside the
dataloader factory, and everything bound to sharded objects (model DCP shards, optimizer,
scheduler) loads in `resume_after_prepare`.
Args:
checkpoint_dir (Path): The checkpoint directory. Should contain a 'training_state' dir.
optimizer (Optimizer): The optimizer to load the state_dict to.
scheduler (LRScheduler | None): The scheduler to load the state_dict to (can be None).
load_optimizer (bool, optional): Whether to load the optimizer state from disk. Defaults to
True. Set to False under FSDP, where the sharded optimizer state must be loaded after
`accelerator.prepare()` via `load_fsdp_optimizer_state` (the optimizer is returned
untouched here).
cfg (TrainPipelineConfig): The resumed training config; `cfg.checkpoint_path` locates
the checkpoint to restore from.
Returns:
int: The training step recorded in the checkpoint (micro-batch counter).
Raises:
NotADirectoryError: If 'checkpoint_dir' doesn't contain a 'training_state' dir
Returns:
tuple[int, Optimizer, LRScheduler | None]: training step, optimizer and scheduler with their
state_dict loaded.
NotADirectoryError: If the checkpoint has no `training_state/` directory.
ValueError: If the resumed topology crosses the sharded/non-sharded boundary relative
to the one recorded in the checkpoint.
"""
training_state_dir = checkpoint_dir / TRAINING_STATE_DIR
training_state_dir = cfg.checkpoint_path / TRAINING_STATE_DIR
if not training_state_dir.is_dir():
raise NotADirectoryError(training_state_dir)
metadata = load_training_metadata(training_state_dir)
_guard_resume_changes(cfg, metadata)
load_rng_state(training_state_dir)
step = load_training_step(training_state_dir)
if load_optimizer:
optimizer = load_optimizer_state(optimizer, training_state_dir)
return metadata["step"]
def _guard_resume_changes(cfg: TrainPipelineConfig, metadata: dict[str, Any]) -> None:
"""Check the resumed run settings against the ones recorded in the checkpoint.
Two tiers, both driven by the checkpoint's recorded parallelism snapshot:
- **Hard error** when the resume crosses the sharded/non-sharded boundary in either
direction: the checkpoint's training-state artifacts only support resuming on the same
kind of topology (resharding works across sizes, not across kinds). Checkpoints without
a recorded snapshot skip this check.
- **One warning** naming every other recorded setting that differs — those changes are
legal (DCP reshards weights and optimizer state across topologies and the sampler offset
adapts), but a changed ``grad_accum_steps`` shifts the optimizer-update cadence, so the
resume says precisely what differs. The sampler-exactness warnings
(``dp_world_size``/``batch_size``) live with the sampler math in the dataloader factory.
Args:
cfg (TrainPipelineConfig): The resumed training config, compared against the settings
recorded in the checkpoint.
metadata (dict[str, Any]): The checkpoint's recorded training metadata, as returned by
`load_training_metadata`.
Raises:
ValueError: If the checkpoint records a sharded topology and the resumed run is
non-sharded, or vice versa.
"""
snapshot = metadata["parallelism"]
if snapshot is not None:
recorded_sharded = (
snapshot.get("dp_shard", 1) != 1
or snapshot.get("ring_degree", 1) * snapshot.get("ulysses_degree", 1) > 1
)
if recorded_sharded != cfg.parallelism.is_sharded:
raise ValueError(
f"Cannot resume: the checkpoint was written with a "
f"{'sharded' if recorded_sharded else 'non-sharded'} topology "
f"(dp_replicate={snapshot.get('dp_replicate')}, dp_shard={snapshot.get('dp_shard')}) "
f"but this run is {'sharded' if cfg.parallelism.is_sharded else 'non-sharded'} "
f"(dp_replicate={cfg.parallelism.dp_replicate}, dp_shard={cfg.parallelism.dp_shard})."
)
recorded = {
"grad_accum_steps": (
metadata["grad_accum_steps"],
cfg.accelerator.gradient_accumulation.steps,
),
}
if snapshot is not None:
recorded.update(
{
"dp_replicate": (snapshot.get("dp_replicate"), cfg.parallelism.dp_replicate),
"dp_shard": (snapshot.get("dp_shard"), cfg.parallelism.dp_shard),
"ring_degree": (
snapshot.get("ring_degree"),
cfg.parallelism.context_parallel.ring_degree,
),
"ulysses_degree": (
snapshot.get("ulysses_degree"),
cfg.parallelism.context_parallel.ulysses_degree,
),
}
)
changed = [f"{key}: {was} -> {now}" for key, (was, now) in recorded.items() if was not in (None, now)]
if changed and is_main_process():
logging.warning(
"Resuming with settings that differ from the checkpoint: " + "; ".join(changed) + ". "
"Topology changes reshard safely via DCP; a changed grad_accum_steps shifts the "
"optimizer-update cadence (the step counter keeps counting micro-batches)."
)
def resume_after_prepare(
cfg: TrainPipelineConfig,
accelerator: "Accelerator",
policy: PreTrainedPolicy,
optimizer: Optimizer | dict[str, Optimizer],
scheduler: LRScheduler | None,
) -> None:
"""Phase 2 — after `accelerator.prepare()`: model (DCP) -> optimizer -> scheduler.
Collective under sharding: call on every rank. The model-weight source follows the
checkpoint's own recorded `checkpoint_format` (on resume, `cfg` was parsed from the
checkpoint's train_config.json): DCP-bearing formats load shards here into the prepared
model (whose construction skipped the safetensors load); the safetensors format was already
loaded by `from_pretrained` before sharding — no model step here.
Args:
cfg (TrainPipelineConfig): The resumed training config; `cfg.checkpoint_path` locates
the checkpoint and `cfg.checkpoint_format` selects the model-weight source.
accelerator (Accelerator): The accelerator the policy was prepared with; it unwraps the
model and owns the DCP load channels.
policy (PreTrainedPolicy): The prepared (possibly sharded) policy to load weights into.
optimizer (Optimizer | dict[str, Optimizer]): The prepared optimizer(s) to restore.
scheduler (LRScheduler | None): The scheduler to restore, or None if the run has none.
Raises:
FileNotFoundError: If the checkpoint format declares DCP model shards but the shard
directory is missing (e.g. it was pruned before upload).
"""
checkpoint_dir = cfg.checkpoint_path
pretrained_dir = checkpoint_dir / PRETRAINED_MODEL_DIR
training_state_dir = checkpoint_dir / TRAINING_STATE_DIR
unwrapped = accelerator.unwrap_model(policy)
sharded = is_sharded_module(unwrapped)
if cfg.checkpoint_format.wants_dcp:
from accelerate.utils.constants import FSDP_MODEL_NAME
dcp_dir = pretrained_dir / f"{FSDP_MODEL_NAME}_0"
if not dcp_dir.is_dir():
raise FileNotFoundError(
f"checkpoint_format={cfg.checkpoint_format.value} declares DCP model shards, "
f"but {dcp_dir} is missing. If the shards were pruned, convert what remains "
"with `lerobot-convert-dcp` or resume from a safetensors checkpoint."
)
load_sharded_model(accelerator, unwrapped, pretrained_dir)
if sharded:
# Requires the prepared optimizer: FSDP2's prepare rebinds param groups to DTensors but
# never migrates optimizer.state — DCP reshards it here (works across topology changes).
load_sharded_optimizer(accelerator, optimizer, unwrapped, training_state_dir)
else:
load_optimizer_state(optimizer, training_state_dir)
if scheduler is not None:
scheduler = load_scheduler_state(scheduler, training_state_dir)
return step, optimizer, scheduler
load_scheduler_state(scheduler, training_state_dir)
def gather_fsdp_state_dicts(model, optimizer) -> tuple[dict, dict]:
"""Gather the full (unsharded) model and optimizer state dicts under FSDP.
`model.state_dict()` and `FSDP.optim_state_dict(...)` are cross-rank collectives, so this must be
called on *every* rank with the prepared (FSDP-wrapped) `model` and `optimizer`. With
`rank0_only=True` and `offload_to_cpu=True`, every rank runs the all-gather but only rank 0
materializes the full dicts (the others get empty dicts) and they are kept on CPU to bound GPU
memory. The returned optimizer state dict is keyed by parameter FQNs and is world-size
independent; `load_fsdp_optimizer_state` reshards it on resume.
Returns:
(model_state_dict, optim_state_dict): full dicts on rank 0, empty dicts on other ranks.
"""
from torch.distributed.fsdp import (
FullOptimStateDictConfig,
FullStateDictConfig,
FullyShardedDataParallel as FSDP, # noqa F401
StateDictType,
)
state_cfg = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
optim_cfg = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=True)
with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, state_cfg, optim_cfg):
model_state_dict = model.state_dict()
optim_state_dict = FSDP.optim_state_dict(model, optimizer)
return model_state_dict, optim_state_dict
def load_fsdp_optimizer_state(model, optimizer, checkpoint_dir: Path) -> None:
"""Load the FSDP optimizer state (saved as safetensors) and reshard it into the optimizer.
This is a cross-rank collective and must be called on every rank *after* `accelerator.prepare()`
with the prepared (FSDP-wrapped) `model` and `optimizer`. The saved state is the full,
world-size-independent optimizer state (keyed by parameter FQNs); `FSDP.optim_state_dict_to_load`
reshards it to the current FSDP topology, so resume on a different number of GPUs works.
"""
from torch.distributed.fsdp import (
FullOptimStateDictConfig,
FullStateDictConfig,
FullyShardedDataParallel as FSDP, # noqa F401
StateDictType,
)
# Every rank reads the same full state from the (shared) checkpoint dir, so rank0_only=False.
full_osd = load_optimizer_state_dict(checkpoint_dir / TRAINING_STATE_DIR)
state_cfg = FullStateDictConfig(rank0_only=False)
optim_cfg = FullOptimStateDictConfig(rank0_only=False)
with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, state_cfg, optim_cfg):
sharded_osd = FSDP.optim_state_dict_to_load(model=model, optim=optimizer, optim_state_dict=full_osd)
optimizer.load_state_dict(sharded_osd)
# ---------------------------------------------------------------------------------------------
# Hub: checkpoint push (resume artifact) and publishing (distribution artifact)
# ---------------------------------------------------------------------------------------------
def push_checkpoint_to_hub(
@@ -311,6 +510,16 @@ def push_checkpoint_to_hub(
The model repo is created idempotently, and the commit is tagged with the
checkpoint step so a checkpoint can be recovered with
--policy.pretrained_revision=<step> instead of a commit sha.
The directory is uploaded verbatim — including DCP shards under the DCP formats: this tree
exists for *resume*, not distribution, and `resolve_resume_checkpoint` downloads it back
symmetrically.
Args:
checkpoint_dir (Path): The local checkpoint step directory to upload.
repo_id (str): The Hub model repo to push to (created idempotently if missing).
private (bool | None): Whether a newly created repo should be private. Defaults to
None (public unless the organization's default is private).
"""
api = HfApi()
api.create_repo(repo_id=repo_id, repo_type="model", private=private, exist_ok=True)
@@ -338,6 +547,16 @@ def resolve_resume_checkpoint(repo_id: str, output_dir: Path) -> Path:
into `output_dir/checkpoints/<step>/`, recreate the local `last` symlink, and return that local
checkpoint dir. Used to resume training from the Hub on a machine (or HF Jobs pod) that does not
have the original local run dir.
Args:
repo_id (str): The Hub model repo holding `checkpoints/<step>/` subtrees.
output_dir (Path): The local run directory to download the checkpoint into.
Returns:
Path: The local checkpoint step directory, `output_dir/checkpoints/<step>`.
Raises:
FileNotFoundError: If the repo contains no checkpoints under `checkpoints/`.
"""
latest = find_latest_hub_checkpoint(repo_id)
if latest is None:
@@ -354,3 +573,214 @@ def resolve_resume_checkpoint(repo_id: str, output_dir: Path) -> Path:
checkpoint_dir = output_dir / latest
update_last_checkpoint(checkpoint_dir)
return checkpoint_dir
def publish_trained_model(
cfg: TrainPipelineConfig,
model: "PreTrainedPolicy | PreTrainedRewardModel",
preprocessor: PolicyProcessorPipeline | None,
postprocessor: PolicyProcessorPipeline | None,
dataset_meta: "LeRobotDatasetMetadata | None",
*,
peft_model: Any | None = None,
) -> None:
"""Publish the complete training bundle as a distributable model repo.
Collective-safe: call on ALL ranks — the model commit gathers sharded weights through
`save_pretrained`; uploads happen on the main process only (gated inside
`HubMixin.push_to_hub` and here). Commits, in order: (1) the model (skipped for PEFT —
adapters replace full weights), (2) the preprocessor, (3) the postprocessor, (4) the bundle
sidecar: README.md model card + train_config.json (+ adapter weights and the wrapped
policy's config in the PEFT case). Every commit uploads a freshly assembled directory, so
a published repo carries only the distributable artifacts.
Args:
cfg (TrainPipelineConfig): The training config; saved as `train_config.json` and used
to render the model card.
model (PreTrainedPolicy | PreTrainedRewardModel): The trained model to publish; its
config supplies the target repo id, visibility, license, and tags.
preprocessor (PolicyProcessorPipeline | None): The preprocessor pipeline to publish
alongside the model, if any.
postprocessor (PolicyProcessorPipeline | None): The postprocessor pipeline to publish
alongside the model, if any.
dataset_meta (LeRobotDatasetMetadata | None): Dataset metadata for the model card, if
available.
peft_model (Any | None): The PEFT wrapper when training adapters; its adapter weights
replace the full model weights in the published repo. Defaults to None.
Raises:
ValueError: If the model config carries no repo id (`--policy.repo_id`).
"""
model_cfg = model.config
repo_id = model_cfg.repo_id
if not repo_id:
raise ValueError("Publishing requires a repo id (--policy.repo_id).")
ignore = ["*.tmp", "*.log"]
if peft_model is None:
# Calls are made on the exact objects that own each method (never through PEFT's
# attribute forwarding), so the peft branch below never touches this path.
model.push_to_hub(repo_id, private=model_cfg.private, ignore_patterns=ignore)
if preprocessor is not None:
preprocessor.push_to_hub(repo_id, private=model_cfg.private)
if postprocessor is not None:
postprocessor.push_to_hub(repo_id, private=model_cfg.private)
if is_main_process():
api = HfApi()
repo_id = api.create_repo(repo_id=repo_id, private=model_cfg.private, exist_ok=True).repo_id
with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
saved_path = Path(tmp) / repo_id
saved_path.mkdir(parents=True, exist_ok=True)
if peft_model is not None:
peft_model.save_pretrained(saved_path) # adapter weights + adapter config
model.config.save_pretrained(saved_path) # PEFT cannot write the policy config
card = generate_model_card(model_cfg, cfg=cfg, dataset_meta=dataset_meta)
card.save(str(saved_path / "README.md"))
cfg.save_pretrained(saved_path) # train_config.json
commit_info = api.upload_folder(
repo_id=repo_id,
repo_type="model",
folder_path=saved_path,
commit_message="Upload model card and train config",
allow_patterns=["*.safetensors", "*.json", "*.yaml", "*.md"],
ignore_patterns=ignore,
)
# Contract: lerobot.jobs.hf.submit_to_hf watches for this exact "Model pushed to <url>"
# line to end a remote run early. Keep the wording and URL format in sync.
logging.info(f"Model pushed to {commit_info.repo_url.url}")
if dist.is_initialized():
dist.barrier()
# ---------------------------------------------------------------------------------------------
# Model card
# ---------------------------------------------------------------------------------------------
_BASE_MODEL_MAPPING = {
"smolvla": "lerobot/smolvla_base",
"pi0": "lerobot/pi0_base",
"pi05": "lerobot/pi05_base",
"pi0_fast": "lerobot/pi0fast-base",
"xvla": "lerobot/xvla-base",
}
def build_card_context(
cfg: TrainPipelineConfig | None,
dataset_meta: "LeRobotDatasetMetadata | None",
input_features: dict | None,
output_features: dict | None,
) -> dict:
"""Collect optional data for the model-card template.
Returns plain values only (no Markdown) — the template in
``lerobot/templates/lerobot_modelcard_template.md`` decides how and whether to show
each one. Everything is best-effort: anything unavailable is left empty/None and the
template simply skips that section, so this never breaks a Hub push.
Args:
cfg (TrainPipelineConfig | None): The training config supplying the training section,
if available.
dataset_meta (LeRobotDatasetMetadata | None): Dataset metadata supplying the dataset,
robot-type, and camera sections, if available.
input_features (dict | None): The policy's input feature declarations, if any.
output_features (dict | None): The policy's output feature declarations, if any.
Returns:
dict: Template context with `training`, `input_features`, `output_features`,
`dataset`, `robot_type`, and `cameras` entries; unavailable pieces stay
empty/None.
"""
context = {
"training": None,
"input_features": input_features or {},
"output_features": output_features or {},
"dataset": None,
"robot_type": None,
"cameras": [],
}
if cfg is not None:
optimizer = getattr(cfg, "optimizer", None)
context["training"] = {
"steps": cfg.steps,
"batch_size": cfg.batch_size,
"seed": cfg.seed,
"optimizer": getattr(optimizer, "type", None) if optimizer else None,
"lr": getattr(optimizer, "lr", None) if optimizer else None,
"lerobot_version": __version__,
}
if dataset_meta is not None:
context["dataset"] = {
"repo_id": dataset_meta.repo_id,
"episodes": dataset_meta.total_episodes,
"frames": dataset_meta.total_frames,
"fps": dataset_meta.fps,
"tasks": [str(task) for task in dataset_meta.tasks.index],
}
context["robot_type"] = dataset_meta.robot_type
context["cameras"] = [key.split(".")[-1] for key in dataset_meta.camera_keys]
return context
def generate_model_card(
model_cfg: PreTrainedConfig | RewardModelConfig,
cfg: TrainPipelineConfig | None = None,
dataset_meta: "LeRobotDatasetMetadata | None" = None,
) -> ModelCard:
"""Render the LeRobot model card for a trained policy or reward model.
A free function on purpose: every template variable comes from arguments — the model
config, the training config, and the dataset metadata — none from a live model, so a card
can also be rendered from a checkpoint's `config.json` alone (see `lerobot-convert-dcp`).
The config type selects the template: reward models get the reward-model card, policies the
policy card with the training/dataset sections.
Args:
model_cfg (PreTrainedConfig | RewardModelConfig): The model config providing type,
license, tags, repo id, and — for policies — the feature declarations.
cfg (TrainPipelineConfig | None, optional): The training config for the training and
dataset card sections. Defaults to None.
dataset_meta (LeRobotDatasetMetadata | None, optional): Dataset metadata for the
dataset card sections. Defaults to None.
Returns:
ModelCard: The rendered and validated LeRobot model card.
"""
model_type = model_cfg.type
base_model = _BASE_MODEL_MAPPING.get(model_type)
if isinstance(model_cfg, RewardModelConfig):
tags = {"robotics", "lerobot", "reward-model", model_type}
template_card = (
files("lerobot.templates")
.joinpath("lerobot_rewardmodel_modelcard_template.md")
.read_text("utf-8")
)
context: dict[str, Any] = {} # the reward template renders from card_data alone
else:
tags = {"robotics", "lerobot", model_type}
template_card = (
files("lerobot.templates").joinpath("lerobot_modelcard_template.md").read_text("utf-8")
)
context = build_card_context(cfg, dataset_meta, model_cfg.input_features, model_cfg.output_features)
# Used by the template to pre-fill commands and the "Fine-tuned from" line.
context["policy_repo_id"] = model_cfg.repo_id
context["base_model"] = base_model
card_data = ModelCardData(
license=model_cfg.license or "apache-2.0",
library_name="lerobot",
pipeline_tag="robotics",
tags=list(tags.union(model_cfg.tags or [])),
model_name=model_type,
datasets=cfg.dataset.repo_id if cfg is not None else None,
base_model=base_model,
)
card = ModelCard.from_template(card_data, template_str=template_card, **context)
card.validate()
return card
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Execution-runtime configuration: everything handed to (or applied by) the `Accelerator`.
Each sub-config mirrors the plain-typed subset of the corresponding accelerate object and
builds it at runtime (the way ``OptimizerConfig.build()`` constructs a ``torch.optim.Optimizer``),
so the whole tree round-trips through the CLI and ``train_config.json`` and parsing a config
never imports accelerate.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING
from lerobot.configs.parallelism import ParallelismConfig
if TYPE_CHECKING:
from accelerate import Accelerator
from accelerate.utils import (
DistributedDataParallelKwargs,
FullyShardedDataParallelPlugin,
GradientAccumulationPlugin,
)
@dataclass
class FSDPConfig:
"""Mirror of the `FullyShardedDataParallelPlugin` subset LeRobot supports (FSDP2 only).
Exactly one wrap policy applies: `wrap_modules` (module *class names* forming the FSDP
units — and, later, the activation-checkpointing units) or `min_num_params` (size-based).
When both are None, the policy's own `_fsdp_wrap_modules` declaration is used; a run where
no wrap source exists at all fails loudly rather than silently wrapping only the root.
"""
reshard_after_forward: bool = True
wrap_modules: list[str] | None = None
min_num_params: int | None = None
cpu_offload: bool = False
# Regex matched against module FQNs to exclude their parameters from sharding.
ignored_modules: str | None = None
def __post_init__(self) -> None:
"""Validate the wrap-policy fields.
Raises:
ValueError: If both ``wrap_modules`` and ``min_num_params`` are set (they are
mutually exclusive wrap policies), or if ``min_num_params`` is < 1.
"""
if self.wrap_modules is not None and self.min_num_params is not None:
raise ValueError(
"fsdp.wrap_modules and fsdp.min_num_params are mutually exclusive wrap policies."
)
if self.min_num_params is not None and self.min_num_params < 1:
raise ValueError(f"fsdp.min_num_params must be >= 1, got {self.min_num_params}.")
def build_plugin(self) -> "FullyShardedDataParallelPlugin":
"""Build the FSDP2 plugin for `Accelerator(fsdp_plugin=...)`.
Returns:
FullyShardedDataParallelPlugin: FSDP2 (`fsdp_version=2`) plugin carrying the
mirrored wrap policy, resharding, CPU-offload, and ignored-modules settings.
"""
from accelerate.utils import FullyShardedDataParallelPlugin
use_size_policy = self.min_num_params is not None
return FullyShardedDataParallelPlugin(
fsdp_version=2,
reshard_after_forward=self.reshard_after_forward,
auto_wrap_policy="size_based_wrap" if use_size_policy else "transformer_based_wrap",
# May legitimately still be None here: the policy-declared default is applied right
# before `accelerator.prepare()` (see lerobot.distributed.factory.set_fsdp_wrap_modules).
transformer_cls_names_to_wrap=list(self.wrap_modules) if self.wrap_modules else None,
min_num_params=self.min_num_params,
cpu_offload=self.cpu_offload,
ignored_modules=self.ignored_modules,
# state_dict_type stays at the FSDP2 default (SHARDED_STATE_DICT) and is never
# switched: full gathers go through torch's state-dict API, which does not consult
# the plugin. activation_checkpointing stays False: AC is LeRobot-owned.
)
@dataclass
class DDPConfig:
"""Mirror of the `DistributedDataParallelKwargs` subset LeRobot exposes."""
# Today's in-script default, kept for models with conditional computation.
find_unused_parameters: bool = True
gradient_as_bucket_view: bool = False
static_graph: bool = False
def build_kwargs_handler(self) -> "DistributedDataParallelKwargs":
"""Build the DDP kwargs handler for `Accelerator(kwargs_handlers=[...])`.
Returns:
DistributedDataParallelKwargs: Handler carrying the mirrored DDP fields, applied
by accelerate when it wraps the model in `DistributedDataParallel`.
"""
from accelerate.utils import DistributedDataParallelKwargs
return DistributedDataParallelKwargs(
find_unused_parameters=self.find_unused_parameters,
gradient_as_bucket_view=self.gradient_as_bucket_view,
static_graph=self.static_graph,
)
@dataclass
class GradientAccumulationConfig:
"""Mirror of the `GradientAccumulationPlugin` subset LeRobot supports.
Only the step count is a knob. ``sync_with_dataloader`` is pinned to False by
:meth:`build_plugin`: the training loop cycles a finite dataloader, so accelerate's default
of syncing at every dataloader end would force an optimizer step at every dataset epoch
boundary instead of every ``steps`` micro-batches.
"""
steps: int = 1
def __post_init__(self) -> None:
"""Validate the accumulation step count.
Raises:
ValueError: If ``steps`` is < 1.
"""
if self.steps < 1:
raise ValueError(f"gradient_accumulation.steps must be >= 1, got {self.steps}.")
def build_plugin(self) -> "GradientAccumulationPlugin":
"""Build the plugin for `Accelerator(gradient_accumulation_plugin=...)`.
A named plugin argument, not a `kwargs_handlers` entry: accelerate consumes this object
through its dedicated constructor parameter — the `KwargsHandler` base class only lends
it `to_kwargs()`, so the consumption site, not the inheritance, decides its role.
Returns:
GradientAccumulationPlugin: Carrying the mirrored step count, with
``sync_with_dataloader=False`` pinned (see the class docstring).
"""
from accelerate.utils import GradientAccumulationPlugin
return GradientAccumulationPlugin(num_steps=self.steps, sync_with_dataloader=False)
@dataclass
class CompileConfig:
"""torch.compile knobs — a configured placeholder: wiring lands in a later round.
The setup-order contract it will follow is already fixed: compile applies
after CP dispatch install and activation checkpointing, before `fully_shard`, regionally
(per wrap unit) — the only combination proven with FSDP2.
"""
enabled: bool = False
backend: str = "inductor"
mode: str | None = None
regional: bool = True
class ActivationCheckpointingMode(str, Enum):
NONE = "none"
FULL = "full"
@dataclass
class ActivationCheckpointingConfig:
"""Activation-checkpointing knobs — a configured placeholder: wiring lands in a later round.
AC units will coincide with the FSDP wrap units (one declaration drives both), applied
before torch.compile and `fully_shard` (the same ordering contract as CompileConfig).
"""
mode: ActivationCheckpointingMode = ActivationCheckpointingMode.NONE
@dataclass
class AcceleratorConfig:
"""Builds the `Accelerator` — the runtime counterpart of the `parallelism` topology.
`mixed_precision` selects accelerate-native AMP for DDP/single-GPU runs and the FSDP2
`MixedPrecisionPolicy` for sharded runs (accelerate derives it). Sharded runs support
"no" and "bf16" only; fp16's GradScaler-over-DTensor path is unverified and fails fast
at config validation.
"""
mixed_precision: str = "no"
gradient_accumulation: GradientAccumulationConfig = field(default_factory=GradientAccumulationConfig)
fsdp: FSDPConfig = field(default_factory=FSDPConfig)
ddp: DDPConfig = field(default_factory=DDPConfig)
compile: CompileConfig = field(default_factory=CompileConfig)
activation_checkpointing: ActivationCheckpointingConfig = field(
default_factory=ActivationCheckpointingConfig
)
def __post_init__(self) -> None:
"""Validate the accelerate-facing scalar fields.
Raises:
ValueError: If ``mixed_precision`` is not one of ``"no"``, ``"fp16"``, ``"bf16"``.
"""
if self.mixed_precision not in ("no", "fp16", "bf16"):
raise ValueError(
f"mixed_precision must be one of 'no', 'fp16', 'bf16', got {self.mixed_precision!r}."
)
def build(self, parallelism: ParallelismConfig, *, cpu: bool = False) -> "Accelerator":
"""Translate the mirrored fields into a ready `Accelerator` (call once per process).
`parallelism` must already be resolved against the world size. The degradation matrix
is encoded here and nowhere else: sharded -> FSDP2 (+HSDP via the accelerate
`ParallelismConfig` mesh), replicated-only -> DDP kwargs, single process -> plain.
Args:
parallelism (ParallelismConfig): The resolved process topology; selects which
accelerate path (FSDP2 mesh, DDP kwargs handler, or plain) is configured.
cpu (bool): Force CPU execution even when CUDA is available. Defaults to False.
Returns:
Accelerator: The configured accelerate entry point for this process.
"""
from accelerate import Accelerator
kwargs: dict = {
# LeRobot steps its scheduler manually once per training step; accelerate must not
# rescale scheduler stepping by num_processes.
"step_scheduler_with_optimizer": False,
"gradient_accumulation_plugin": self.gradient_accumulation.build_plugin(),
"mixed_precision": self.mixed_precision,
"cpu": cpu,
}
if parallelism.is_sharded:
kwargs["fsdp_plugin"] = self.fsdp.build_plugin()
kwargs["parallelism_config"] = _accelerate_parallelism_config(parallelism)
elif parallelism.is_replicated_only:
kwargs["kwargs_handlers"] = [self.ddp.build_kwargs_handler()]
return Accelerator(**kwargs)
def _accelerate_parallelism_config(parallelism: ParallelismConfig) -> object:
"""LeRobot topology -> accelerate `ParallelismConfig`.
CP is declared honestly (`cp_size = ring x ulysses`) so accelerate builds the canonical
mesh, folds CP into the FSDP shard group (`dp_shard_cp`), and duplicates batches within CP
groups. The ring/ulysses sub-structure stays private to `lerobot.distributed.ParallelDims`.
Args:
parallelism (ParallelismConfig): The resolved LeRobot topology to translate.
Returns:
object: The accelerate `ParallelismConfig` mirroring `dp_replicate`, `dp_shard`, and
the collapsed `cp_size` (annotated as `object` so importing this module never
imports accelerate).
"""
from accelerate.parallelism_config import ParallelismConfig as AccelerateParallelismConfig
return AccelerateParallelismConfig(
dp_replicate_size=parallelism.dp_replicate,
dp_shard_size=parallelism.dp_shard,
cp_size=parallelism.cp_size,
)
+13
View File
@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from dataclasses import dataclass, field
from lerobot.transforms import ImageTransformsConfig
@@ -21,6 +22,8 @@ from lerobot.utils.import_utils import get_safe_default_video_backend
from .video import DEFAULT_DEPTH_UNIT, DEPTH_METER_UNIT, DEPTH_MILLIMETER_UNIT
logger = logging.getLogger(__name__)
@dataclass
class DatasetConfig:
@@ -36,6 +39,8 @@ class DatasetConfig:
# looked up under $HF_LEROBOT_HOME/repo_id and Hub downloads use a revision-safe cache under $HF_LEROBOT_HOME/hub.
root: str | None = None
episodes: list[int] | None = None
# Episode indices to drop (e.g. corrupt or heterogeneous ones). Applied on top of `episodes`.
exclude_episodes: list[int] | None = None
image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig)
revision: str | None = None
use_imagenet_stats: bool = True
@@ -75,6 +80,14 @@ class DatasetConfig:
if len(self.episodes) != len(set(self.episodes)):
duplicates = sorted({ep for ep in self.episodes if self.episodes.count(ep) > 1})
raise ValueError(f"Episode indices contain duplicates: {duplicates}")
if self.exclude_episodes is not None:
negative_episodes = [episode for episode in self.exclude_episodes if episode < 0]
if negative_episodes:
logger.warning(
"Ignoring negative exclude_episodes entries: %s",
negative_episodes,
)
self.exclude_episodes = [episode for episode in self.exclude_episodes if episode >= 0]
@dataclass
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Declarative process topology for distributed training and inference.
The mesh convention (canonical row-major rank layout, outermost first)::
(dp_replicate, dp_shard, ring, ulysses)
- ``dp_replicate x dp_shard`` is the data-parallel world: HSDP replicates over
``dp_replicate`` and shards parameters over ``dp_shard``. FSDP2's actual shard
group folds context parallelism in (``dp_shard x ring x ulysses``), matching
accelerate's ``dp_shard_cp`` flattening and torchtitan's ``fsdp`` axis.
- ``ring`` is the outer and ``ulysses`` the inner context-parallel dim
(diffusers convention: ulysses all-to-all exchanges run over adjacent, typically
NVLink-connected ranks).
- ``cfg_parallel`` (classifier-free-guidance parallelism) is a branch-parallel,
inference-only dim that sits between dp and the sequence dims. It never
affects weight sharding or checkpoints.
This module is pure configuration: plain-typed dataclasses that draccus can
round-trip through the CLI and ``train_config.json``. Runtime objects (device
meshes, process groups) live in :mod:`lerobot.distributed`.
"""
import os
from dataclasses import dataclass, field
@dataclass
class ContextParallelConfig:
"""Ring x Ulysses context parallelism (sequence parallelism for attention).
Both degrees are configured placeholders in this release: the CP engine is not implemented
yet, and enabling either degree > 1 fails fast at config validation. The fields exist now so
that the CLI surface, checkpoint metadata, and mesh math are stable when the engine lands.
"""
ring_degree: int = 1
ulysses_degree: int = 1
def __post_init__(self) -> None:
"""Validate the declared context-parallel degrees.
Raises:
ValueError: If ``ring_degree`` or ``ulysses_degree`` is < 1.
"""
if self.ring_degree < 1 or self.ulysses_degree < 1:
raise ValueError(
f"Context-parallel degrees must be >= 1, got ring_degree={self.ring_degree}, "
f"ulysses_degree={self.ulysses_degree}."
)
@property
def size(self) -> int:
"""Total number of ranks a full sequence is sharded across."""
return self.ring_degree * self.ulysses_degree
@dataclass
class ParallelismConfig:
"""Degrees of every parallelism dim. Invariant: their product equals the world size.
Degradations are expressed purely through the degrees (no mode flags):
- single process: all degrees 1;
- DDP: ``dp_replicate == world_size`` (auto-filled when every sharding field is left at its
default — plain ``torchrun`` keeps today's out-of-the-box behavior);
- FSDP: ``dp_shard > 1`` (or ``-1`` to fill the remaining world into the shard dim);
- HSDP: ``dp_replicate > 1`` and ``dp_shard > 1``.
``resolve()`` turns the declared degrees into concrete ones once the world size is known and
is the single place the world-size equation is enforced. It is called by
:func:`lerobot.distributed.factory.make_accelerator`; the config is inert until then.
"""
dp_replicate: int = 1
# -1 is an explicit opt-in sentinel: shard over world_size // (dp_replicate * cp).
dp_shard: int = 1
context_parallel: ContextParallelConfig = field(default_factory=ContextParallelConfig)
# Classifier-free-guidance parallelism — inference-only (cosmos/vllm-omni precedent:
# cond/uncond branches on different ranks). Reserved for the serving round; training
# validates it to 1. Meaningful values are 1 or 2 (Cosmos3 has two CFG branches).
cfg_parallel: int = 1
def __post_init__(self) -> None:
"""Validate the declared degrees (world-size-independent checks only).
Raises:
ValueError: If ``dp_replicate`` is < 1, ``dp_shard`` is neither >= 1 nor the
``-1`` infer sentinel, or ``cfg_parallel`` is not 1 or 2.
"""
if self.dp_replicate < 1:
raise ValueError(f"dp_replicate must be >= 1, got {self.dp_replicate}.")
if self.dp_shard < 1 and self.dp_shard != -1:
raise ValueError(f"dp_shard must be >= 1, or -1 to infer, got {self.dp_shard}.")
if self.cfg_parallel not in (1, 2):
raise ValueError(f"cfg_parallel must be 1 or 2, got {self.cfg_parallel}.")
@property
def cp_size(self) -> int:
"""Total context-parallel size (``ring_degree * ulysses_degree``)."""
return self.context_parallel.size
@property
def is_sharded(self) -> bool:
"""True when the run uses FSDP2 (parameters sharded); selects the sharded engine path."""
return self.dp_shard != 1 or self.cp_size > 1
@property
def is_replicated_only(self) -> bool:
"""True for plain DDP (weights replicated, no sharding)."""
return not self.is_sharded and self.dp_replicate > 1
@property
def dp_world_size(self) -> int:
"""Number of distinct data-parallel workers (batches are sharded this many ways).
Returns:
int: ``dp_replicate * dp_shard``.
Raises:
RuntimeError: If accessed while ``dp_shard`` is still the ``-1`` sentinel, i.e.
before :meth:`resolve` has bound the degrees to a world size.
"""
if self.dp_shard == -1:
raise RuntimeError("dp_world_size is undefined before resolve() fills dp_shard=-1.")
return self.dp_replicate * self.dp_shard
def resolve(self, world_size: int) -> None:
"""Bind the declared degrees to a concrete world size (idempotent).
Fills the ``dp_shard=-1`` sentinel, auto-fills ``dp_replicate`` for the DDP degradation,
and enforces ``dp_replicate * dp_shard * cp == world_size`` with every degree echoed on
failure.
Args:
world_size (int): Total number of launched processes (torchrun's ``WORLD_SIZE``).
Raises:
ValueError: If a context-parallel degree is > 1 (the CP engine is not implemented
yet), if ``dp_shard=-1`` cannot be inferred because ``world_size`` is not
divisible by ``dp_replicate * cp``, or if the resolved degrees do not multiply
to ``world_size``.
"""
if self.cp_size > 1:
raise ValueError(
"Context parallelism is not implemented yet: ring_degree and ulysses_degree "
"must be 1. The fields are reserved for the CP engine round."
)
if self.is_sharded:
if self.dp_shard == -1:
self.dp_shard, remainder = divmod(world_size, self.dp_replicate * self.cp_size)
if remainder or self.dp_shard < 1:
raise ValueError(
f"Cannot infer dp_shard: world_size={world_size} is not divisible by "
f"dp_replicate={self.dp_replicate} * cp={self.cp_size}."
)
elif self.dp_replicate == 1:
# Untouched config on a multi-process launch: fill the DDP degradation.
self.dp_replicate = world_size
total = self.dp_replicate * self.dp_shard * self.cp_size
if total != world_size:
raise ValueError(
f"Parallelism degrees do not multiply to the world size: dp_replicate="
f"{self.dp_replicate} * dp_shard={self.dp_shard} * ring="
f"{self.context_parallel.ring_degree} * ulysses="
f"{self.context_parallel.ulysses_degree} = {total} != WORLD_SIZE={world_size}."
)
def world_size_from_env() -> int:
"""World size as set by torchrun (or 1 outside distributed launches).
Returns:
int: The ``WORLD_SIZE`` environment variable, or 1 when unset.
"""
return int(os.environ.get("WORLD_SIZE", "1"))
+24 -7
View File
@@ -23,6 +23,7 @@ from typing import Any, Literal, get_args
MessageRole = Literal["user", "assistant", "system", "tool"]
MessageStream = Literal["high_level", "low_level"]
RecipeRoute = Literal["vqa"]
DEFAULT_BINDINGS = {
"subtask": "active_at(t, style=subtask)",
@@ -40,6 +41,7 @@ discovery (here) and rendered-message substitution (in ``language_render``)."""
_VALID_ROLES = frozenset(get_args(MessageRole))
_VALID_STREAMS = frozenset(get_args(MessageStream))
_VALID_ROUTES = frozenset(get_args(RecipeRoute))
@dataclass
@@ -78,7 +80,7 @@ class MessageTurn:
raise ValueError(f"Unsupported message stream: {self.stream!r}")
if self.content is None and self.tool_calls_from is None:
raise ValueError("MessageTurn.content is required unless tool_calls_from is set.")
if self.content is not None and not isinstance(self.content, (str, list)):
if self.content is not None and not isinstance(self.content, str | list):
raise TypeError("MessageTurn.content must be a string, a list of HF-style blocks, or None.")
if isinstance(self.content, list):
for block in self.content:
@@ -99,13 +101,16 @@ class TrainingRecipe:
A recipe is either a *message recipe* (``messages`` plus optional
``bindings``) or a *blend recipe* (``blend`` mapping names to weighted
sub-recipes). ``weight`` is only meaningful inside a blend.
sub-recipes). ``weight`` and ``route`` are only meaningful inside a blend;
``route: vqa`` gives sparse VQA annotations priority over normal weighted
selection.
"""
messages: list[MessageTurn] | None = None
bindings: dict[str, str] | None = None
blend: dict[str, TrainingRecipe] | None = None
weight: float | None = None
route: RecipeRoute | None = None
def __post_init__(self) -> None:
"""Validate that exactly one of ``messages`` or ``blend`` is set."""
@@ -113,6 +118,10 @@ class TrainingRecipe:
raise ValueError("TrainingRecipe must set only one of messages or blend.")
if self.messages is None and self.blend is None:
raise ValueError("TrainingRecipe must set one of messages or blend.")
if self.route is not None and self.route not in _VALID_ROUTES:
raise ValueError(f"Unsupported recipe route: {self.route!r}")
if self.blend is not None and self.route is not None:
raise ValueError("TrainingRecipe.route may only be set on a message recipe inside a blend.")
if self.messages is not None:
self._validate_message_recipe()
@@ -147,8 +156,9 @@ class TrainingRecipe:
return cls.from_dict(data)
def _validate_message_recipe(self) -> None:
"""Ensure every templated binding is known and at least one turn is a target."""
assert self.messages is not None
"""Validate bindings and require text or low-level action supervision."""
if self.messages is None:
raise ValueError("Cannot validate a message recipe without messages.")
known_bindings = set(DEFAULT_BINDINGS) | set(self.bindings or {}) | {"task"}
for turn in self.messages:
@@ -156,12 +166,19 @@ class TrainingRecipe:
if missing:
raise ValueError(f"MessageTurn references unknown binding(s): {sorted(missing)}")
if not any(turn.target for turn in self.messages):
raise ValueError("Message recipes must contain at least one target turn.")
has_target = any(turn.target for turn in self.messages)
has_low_level = any(turn.stream == "low_level" for turn in self.messages)
if not (has_target or has_low_level):
raise ValueError(
"Message recipes must contain at least one supervised turn — "
"either ``target: true`` (text CE) or ``stream: low_level`` "
"(flow/action loss)."
)
def _validate_blend_recipe(self) -> None:
"""Ensure each blend component is a non-empty, weighted message recipe."""
assert self.blend is not None
if self.blend is None:
raise ValueError("Cannot validate a blend recipe without blend components.")
if not self.blend:
raise ValueError("Blend recipes must contain at least one component.")
+16
View File
@@ -0,0 +1,16 @@
# Predicts subtasks from tasks and trains subtask-conditioned action flow without memory or plans.
# Requires `subtask` annotations; samples with missing `if_present` bindings do not render.
blend:
high_level_subtask:
weight: 0.30
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: assistant, content: "${subtask}", stream: high_level, target: true, if_present: subtask}
low_level_execution:
weight: 0.70
messages:
# The low-level stream trains action flow on the generated or annotated subtask.
- {role: user, content: "${subtask}", stream: low_level, if_present: subtask}
@@ -0,0 +1,13 @@
# Paper-style joint sequence (pi0.5 §IV-B): one sample supervises the subtask
# text with CE and, because the assistant turn is part of the prefix, conditions
# the FAST and flow action losses on the same annotated subtask in one forward.
# The supervised span is attended causally; the action losses see task + subtask.
#
# Pair with `--policy.joint_subtask_conditioning=true` at inference so the flow
# prefix reproduces this layout (task turn with state + causal generated subtask).
# Samples without a `subtask` annotation fall back to a plain task-prompt
# low-level sample via `if_present`.
messages:
- {role: user, content: "${task}", stream: low_level}
- {role: assistant, content: "${subtask}", stream: low_level, target: true, if_present: subtask}
@@ -0,0 +1,30 @@
# Trains subtask prediction, subtask-conditioned action flow, and memory updates without plans.
# Requires `subtask` and `memory`; missing `if_present` bindings skip the affected sub-recipe.
blend:
high_level_subtask:
weight: 0.25
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: assistant, content: "${subtask}", stream: high_level, target: true, if_present: subtask}
low_level_execution:
weight: 0.60
messages:
# The low-level stream trains action flow on the generated or annotated subtask.
- {role: user, content: "${subtask}", stream: low_level, if_present: subtask}
memory_update:
# `active_at` densifies sparse boundaries while preserving the prior-memory/subtask mapping.
# Inference controls update timing through `subtask_change` events.
weight: 0.15
bindings:
prior_memory: "nth_prev(style=memory, offset=1)"
current_memory: "active_at(t, style=memory)"
completed_subtask: "nth_prev(style=subtask, offset=1)"
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: assistant, content: "Previous memory: ${prior_memory}", stream: high_level, if_present: prior_memory}
- {role: user, content: "Completed subtask: ${completed_subtask}", stream: high_level, if_present: completed_subtask}
- {role: assistant, content: "${current_memory}", stream: high_level, target: true, if_present: current_memory}
@@ -0,0 +1,72 @@
# Adds memory, spoken interjection responses, and camera-grounded VQA to subtask/action training.
# Missing optional annotations skip only their sub-recipe; `say` tool calls tokenize as `<say>...</say>`.
blend:
high_level_subtask:
weight: 0.25
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: assistant, content: "${subtask}", stream: high_level, target: true, if_present: subtask}
low_level_execution:
weight: 0.40
messages:
# The low-level stream trains action flow on the generated or annotated subtask.
- {role: user, content: "${subtask}", stream: low_level, if_present: subtask}
memory_update:
# `active_at` densifies sparse boundaries while preserving the prior-memory/subtask mapping.
# Inference controls update timing through `subtask_change` events.
weight: 0.10
bindings:
prior_memory: "nth_prev(style=memory, offset=1)"
current_memory: "active_at(t, style=memory)"
completed_subtask: "nth_prev(style=subtask, offset=1)"
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: assistant, content: "Previous memory: ${prior_memory}", stream: high_level, if_present: prior_memory}
- {role: user, content: "Completed subtask: ${completed_subtask}", stream: high_level, if_present: completed_subtask}
- {role: assistant, content: "${current_memory}", stream: high_level, target: true, if_present: current_memory}
user_interjection_response:
weight: 0.10
bindings:
interjection: "emitted_at(t, style=interjection)"
speech: "emitted_at(t, role=assistant, tool_name=say)"
messages:
- {role: user, content: "${task}", stream: high_level}
- {role: user, content: "${interjection}", stream: high_level, if_present: interjection}
# The assistant target is a `say` tool call flattened to a `<say>...</say>` marker.
- {role: assistant, stream: high_level, target: true, if_present: speech, tool_calls_from: speech}
# Each camera uses a separate VQA sub-recipe for view-specific binding.
ask_vqa_top:
weight: 0.075
route: vqa
bindings:
vqa_query: "emitted_at(t, style=vqa, role=user, camera=observation.images.front)"
vqa: "emitted_at(t, style=vqa, role=assistant, camera=observation.images.front)"
messages:
- role: user
stream: high_level
if_present: vqa_query
content:
- {type: image, feature: observation.images.front}
- {type: text, text: "${vqa_query}"}
- {role: assistant, content: "${vqa}", stream: high_level, target: true, if_present: vqa}
ask_vqa_wrist:
weight: 0.075
route: vqa
bindings:
vqa_query: "emitted_at(t, style=vqa, role=user, camera=observation.images.wrist)"
vqa: "emitted_at(t, style=vqa, role=assistant, camera=observation.images.wrist)"
messages:
- role: user
stream: high_level
if_present: vqa_query
content:
- {type: image, feature: observation.images.wrist}
- {type: text, text: "${vqa_query}"}
- {role: assistant, content: "${vqa}", stream: high_level, target: true, if_present: vqa}
+92
View File
@@ -18,6 +18,7 @@ import multiprocessing
import os
import tempfile
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
@@ -26,6 +27,8 @@ from huggingface_hub import hf_hub_download
from huggingface_hub.errors import HfHubHTTPError
from lerobot import envs
from lerobot.configs.accelerator import AcceleratorConfig, ActivationCheckpointingMode
from lerobot.configs.parallelism import ParallelismConfig
from lerobot.optim import LRSchedulerConfig, OptimizerConfig
from lerobot.utils.constants import PRETRAINED_MODEL_DIR
from lerobot.utils.hub import HubMixin, find_latest_hub_checkpoint
@@ -39,6 +42,34 @@ from .rewards import RewardModelConfig
TRAIN_CONFIG_NAME = "train_config.json"
class CheckpointFormat(str, Enum):
"""Model-artifact format inside training checkpoints.
Selects only the *model* artifact; the training_state layout is format-independent (the
optimizer channel is always DCP under sharded runs, safetensors+json otherwise).
- SAFETENSORS (default): a full `model.safetensors` — maximum compatibility, one gather per
save under sharding.
- DCP: sharded `pytorch_model_fsdp_0/*.distcp` only — fastest save/resume; convert with
`lerobot-convert-dcp` before distributing.
- SAFETENSORS_AND_DCP: both artifacts, written independently.
"""
SAFETENSORS = "safetensors"
DCP = "dcp"
SAFETENSORS_AND_DCP = "safetensors_dcp"
@property
def wants_safetensors(self) -> bool:
"""True when a full `model.safetensors` artifact should be written."""
return self in (CheckpointFormat.SAFETENSORS, CheckpointFormat.SAFETENSORS_AND_DCP)
@property
def wants_dcp(self) -> bool:
"""True when sharded DCP model shards (`pytorch_model_fsdp_0/`) should be written."""
return self in (CheckpointFormat.DCP, CheckpointFormat.SAFETENSORS_AND_DCP)
def _migrate_legacy_rabc_fields(config: dict[str, Any]) -> dict[str, Any] | None:
"""Return migrated payload for legacy RA-BC fields, or None when no migration is needed."""
legacy_fields = (
@@ -121,9 +152,16 @@ class TrainPipelineConfig(HubMixin):
# Checkpoint is saved every `save_freq` training iterations and after the last training step.
# A non-positive value disables periodic saving, keeping only the final checkpoint.
save_freq: int = 20_000
# Model-artifact format inside checkpoints; non-default values require a sharded run.
checkpoint_format: CheckpointFormat = CheckpointFormat.SAFETENSORS
use_policy_training_preset: bool = True
optimizer: OptimizerConfig | None = None
scheduler: LRSchedulerConfig | None = None
# Process topology: dp_replicate / dp_shard (HSDP) and context-parallel degree placeholders.
parallelism: ParallelismConfig = field(default_factory=ParallelismConfig)
# Execution runtime handed to the Accelerator: mixed precision, gradient accumulation,
# FSDP/DDP tuning knobs, compile & activation-checkpointing placeholders.
accelerator: AcceleratorConfig = field(default_factory=AcceleratorConfig)
eval: EvalConfig = field(default_factory=EvalConfig)
wandb: WandBConfig = field(default_factory=WandBConfig)
peft: PeftConfig | None = None
@@ -291,6 +329,60 @@ class TrainPipelineConfig(HubMixin):
if self.save_checkpoint_to_hub and not (self.policy is not None and self.policy.repo_id):
raise ValueError("save_checkpoint_to_hub requires --policy.repo_id.")
self._validate_distributed()
def _validate_distributed(self) -> None:
"""Fail-fasts for the distributed-training scope.
Raises:
ValueError: If the config requests anything outside the verified scope: context
parallelism or CFG parallelism (reserved placeholders), the compile or
activation-checkpointing placeholders, a DCP checkpoint format on a
non-sharded run, or — under sharded training — fp16 mixed precision, PEFT,
reward-model training, in-training environment evaluation, or multi-optimizer
configs.
"""
if self.parallelism.cp_size > 1:
raise ValueError(
"Context parallelism is not implemented yet: --parallelism.context_parallel.* "
"degrees must be 1 (reserved for the CP engine round)."
)
if self.parallelism.cfg_parallel != 1:
raise ValueError(
"CFG parallelism is inference-only and must be 1 for training "
"(cfg_parallel is reserved for the serving round)."
)
if self.accelerator.compile.enabled:
raise ValueError("--accelerator.compile is a placeholder and not wired yet.")
if self.accelerator.activation_checkpointing.mode is not ActivationCheckpointingMode.NONE:
raise ValueError("--accelerator.activation_checkpointing is a placeholder and not wired yet.")
if self.checkpoint_format is not CheckpointFormat.SAFETENSORS and not self.parallelism.is_sharded:
raise ValueError(
f"checkpoint_format={self.checkpoint_format.value} requires a sharded run "
"(--parallelism.dp_shard != 1); non-sharded checkpoints are always safetensors."
)
if self.parallelism.is_sharded:
if self.accelerator.mixed_precision == "fp16":
raise ValueError(
"fp16 is not supported under sharded training (GradScaler over DTensor "
"gradients is unverified); use bf16 or full precision."
)
if self.peft is not None:
raise ValueError("PEFT is not supported under sharded training yet.")
if self.is_reward_model_training:
raise ValueError(
"Reward-model training is not supported under sharded training yet "
"(reward models declare no FSDP wrap units and have no sharded save path)."
)
if self.env is not None and self.env_eval_freq > 0:
raise ValueError(
"In-training environment evaluation is not supported under sharded training "
"(a rank-0-only rollout of a sharded model deadlocks on collectives); set "
"--env_eval_freq=0 and evaluate with lerobot-eval on saved checkpoints."
)
if self.optimizer is not None and self.optimizer.builds_multiple_optimizers:
raise ValueError("Multi-optimizer configs are not supported under sharded training.")
@classmethod
def __get_path_fields__(cls) -> list[str]:
"""Keys for draccus pretrained-path loading."""
@@ -76,7 +76,7 @@ import torch
from pydantic import BaseModel, Field
from transformers import AutoProcessor, Qwen3VLMoeForConditionalGeneration
from lerobot.datasets import LeRobotDataset
from lerobot.datasets import LeRobotDataset, resolve_episode_indices
# Pydantic Models for SARM Subtask Annotation
@@ -1049,7 +1049,10 @@ def main():
torch_dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16, "float32": torch.float32}[args.dtype]
# Determine episodes
episode_indices = args.episodes or list(range(dataset.meta.total_episodes))
resolved_episodes = resolve_episode_indices(args.episodes, dataset.meta.total_episodes)
episode_indices = (
resolved_episodes if resolved_episodes is not None else list(range(dataset.meta.total_episodes))
)
existing_annotations = load_annotations_from_dataset(dataset.root, prefix="sparse")
if args.skip_existing:
+2 -1
View File
@@ -52,7 +52,7 @@ from .pipeline_features import aggregate_pipeline_dataset_features, create_initi
from .pyav_utils import check_video_encoder_parameters_pyav, detect_available_encoders_pyav
from .sampler import EpisodeAwareSampler, compute_sampler_state
from .streaming_dataset import StreamingLeRobotDataset
from .utils import DEFAULT_EPISODES_PATH, create_lerobot_dataset_card
from .utils import DEFAULT_EPISODES_PATH, create_lerobot_dataset_card, resolve_episode_indices
from .video_utils import VideoEncodingManager
# NOTE: Low-level I/O functions (cast_stats_to_numpy, get_parquet_file_size_in_mb, etc.)
@@ -97,6 +97,7 @@ __all__ = [
"reencode_dataset",
"remove_feature",
"resolve_delta_timestamps",
"resolve_episode_indices",
"safe_stop_image_writer",
"split_dataset",
"write_stats",
+26 -1
View File
@@ -39,6 +39,7 @@ from .io_utils import (
hf_transform_to_torch,
load_nested_dataset,
)
from .utils import resolve_episode_indices
from .video_utils import decode_video_frames
@@ -83,7 +84,7 @@ class DatasetReader:
"""
self._meta = meta
self.root = root
self.episodes = episodes
self.episodes = resolve_episode_indices(episodes, meta.total_episodes)
self._tolerance_s = tolerance_s
self._video_backend = video_backend
if image_transforms is not None and not callable(image_transforms):
@@ -163,10 +164,34 @@ class DatasetReader:
def _load_hf_dataset(self) -> datasets.Dataset:
"""hf_dataset contains all the observations, states, actions, rewards, etc."""
features = get_hf_features_from_features(self._meta.features)
self._validate_language_columns_declared(features)
hf_dataset = load_nested_dataset(self.root / "data", features=features, episodes=self.episodes)
hf_dataset.set_transform(hf_transform_to_torch)
return hf_dataset
def _validate_language_columns_declared(self, features: datasets.Features) -> None:
"""Require language columns stored in Parquet to be declared in metadata."""
# Leave empty datasets to fail through the normal loading path.
try:
sample = next((self.root / "data").glob("*/*.parquet"))
except StopIteration:
return
from pyarrow import parquet as _pq # noqa: PLC0415
# LeRobot shards are schema-uniform, so one schema represents the dataset.
schema_names = set(_pq.read_schema(sample).names)
from .language import LANGUAGE_COLUMNS # noqa: PLC0415
missing = sorted(set(LANGUAGE_COLUMNS) & schema_names - set(features))
if missing:
raise ValueError(
f"Dataset Parquet files contain language feature(s) missing from metadata: {missing}. "
"Metadata must describe the stored data; add the entries returned by "
"lerobot.datasets.language.language_feature_info() to meta/info.json['features'] "
"or rerun the annotation pipeline's metadata synchronization."
)
def _check_cached_episodes_sufficient(self) -> bool:
"""Check if the cached dataset contains all requested episodes and their video files."""
if self.hf_dataset is None or len(self.hf_dataset) == 0:
+6 -2
View File
@@ -29,6 +29,7 @@ from .dataset_metadata import LeRobotDatasetMetadata
from .lerobot_dataset import LeRobotDataset
from .multi_dataset import MultiLeRobotDataset
from .streaming_dataset import StreamingLeRobotDataset
from .utils import resolve_episode_indices
def resolve_delta_timestamps(
@@ -90,6 +91,9 @@ def make_dataset(cfg: TrainPipelineConfig) -> LeRobotDataset | MultiLeRobotDatas
repo_type=cfg.dataset.repo_type,
)
delta_timestamps = resolve_delta_timestamps(cfg.trainable_config, ds_meta)
episodes = resolve_episode_indices(
cfg.dataset.episodes, ds_meta.total_episodes, cfg.dataset.exclude_episodes
)
if not cfg.dataset.streaming:
if cfg.dataset.repo_type == "bucket":
raise ValueError(
@@ -98,7 +102,7 @@ def make_dataset(cfg: TrainPipelineConfig) -> LeRobotDataset | MultiLeRobotDatas
dataset = LeRobotDataset(
cfg.dataset.repo_id,
root=cfg.dataset.root,
episodes=cfg.dataset.episodes,
episodes=episodes,
delta_timestamps=delta_timestamps,
image_transforms=image_transforms,
revision=cfg.dataset.revision,
@@ -111,7 +115,7 @@ def make_dataset(cfg: TrainPipelineConfig) -> LeRobotDataset | MultiLeRobotDatas
dataset = StreamingLeRobotDataset(
cfg.dataset.repo_id,
root=cfg.dataset.root,
episodes=cfg.dataset.episodes,
episodes=episodes,
delta_timestamps=delta_timestamps,
image_transforms=image_transforms,
revision=cfg.dataset.revision,
+84 -12
View File
@@ -162,14 +162,32 @@ def render_sample(
task: str | None = None,
dataset_ctx: Any | None = None,
) -> RenderedMessages | None:
"""Render the chat-style messages for a single dataset sample.
"""Render recipe-defined messages and supervision for one dataset sample.
Resolves the recipe's bindings against ``persistent`` and ``events`` rows
at frame timestamp ``t``, then expands the recipe's message templates.
Returns ``None`` if the resolved sample contains no target message.
Resolves bindings against ``persistent`` and ``events`` at frame timestamp
``t``. Blend recipes first route matching sparse VQA annotations, then use
deterministic weighted selection for the remaining samples. Returns
``None`` when the selected recipe provides no text or low-level action
supervision for this sample.
"""
persistent_rows = _normalize_rows(persistent or [])
event_rows = _normalize_rows(events or [])
# Route sparse VQA frames to a matching view-specific component before weighted selection.
# This avoids dropping annotated frames or selecting VQA without annotations.
if recipe.blend is not None:
vqa_rendered = _render_vqa_if_present(
recipe,
persistent=persistent_rows,
events=event_rows,
t=t,
sample_idx=sample_idx,
task=task,
dataset_ctx=dataset_ctx,
)
if vqa_rendered is not None:
return vqa_rendered
selected_recipe = _select_recipe(recipe, sample_idx)
bindings = _resolve_bindings(
selected_recipe,
@@ -183,6 +201,58 @@ def render_sample(
return _render_message_recipe(selected_recipe, bindings)
def _render_vqa_if_present(
recipe: TrainingRecipe,
*,
persistent: Sequence[LanguageRow],
events: Sequence[LanguageRow],
t: float,
sample_idx: int,
task: str | None,
dataset_ctx: Any | None,
) -> RenderedMessages | None:
"""Render a matching VQA component, or return ``None`` for normal selection.
Multiple matching views are selected deterministically by relative weight.
"""
if recipe.blend is None:
return None
renderable: list[tuple[float, RenderedMessages]] = []
for component in recipe.blend.values():
if component.route != "vqa":
continue
bindings = _resolve_bindings(
component,
persistent=persistent,
events=events,
t=t,
sample_idx=sample_idx,
task=task,
dataset_ctx=dataset_ctx,
)
rendered = _render_message_recipe(component, bindings)
if rendered is not None:
if component.weight is None:
raise ValueError("Routed VQA blend components must define a weight.")
renderable.append((component.weight, rendered))
if not renderable:
return None
if len(renderable) == 1:
return renderable[0][1]
# Choose among matching cameras by their validated positive relative weights.
total = sum(weight for weight, _ in renderable)
digest = hashlib.blake2b(f"vqa:{sample_idx}".encode(), digest_size=8).digest()
draw = int.from_bytes(digest, "big") / 2**64 * total
cumulative = 0.0
for weight, rendered in renderable:
cumulative += weight
if draw < cumulative:
return rendered
return renderable[-1][1]
def _select_recipe(recipe: TrainingRecipe, sample_idx: int) -> TrainingRecipe:
"""Pick a deterministic blend component for ``sample_idx`` (or return ``recipe``)."""
if recipe.blend is None:
@@ -201,7 +271,8 @@ def _select_recipe(recipe: TrainingRecipe, sample_idx: int) -> TrainingRecipe:
cumulative += component.weight or 0.0
if draw < cumulative:
return component
assert last_component is not None
if last_component is None:
raise ValueError("Blend recipes must contain at least one component.")
return last_component
@@ -321,7 +392,8 @@ def _render_message_recipe(
bindings: dict[str, LanguageRow | str | None],
) -> RenderedMessages | None:
"""Expand ``recipe.messages`` into rendered chat messages using ``bindings``."""
assert recipe.messages is not None
if recipe.messages is None:
raise ValueError("Cannot render a blend recipe as a message recipe.")
messages: list[dict[str, Any]] = []
streams: list[str | None] = []
target_indices: list[int] = []
@@ -346,7 +418,9 @@ def _render_message_recipe(
if turn.target:
target_indices.append(message_idx)
if not target_indices:
# Keep samples with either text targets or low-level action supervision.
has_low_level = any(stream == "low_level" for stream in streams)
if not target_indices and not has_low_level:
return None
rendered = {
@@ -403,14 +477,12 @@ def _validate_rendered(rendered: RenderedMessages) -> None:
if len(streams) != len(messages):
raise ValueError("message_streams must be aligned with messages.")
if not target_indices:
raise ValueError("Rendered samples must contain at least one target message.")
# Require text or low-level action supervision.
if not target_indices and not any(s == "low_level" for s in streams):
raise ValueError("Rendered samples must contain a target message or a low_level-stream message.")
for idx in target_indices:
if idx < 0 or idx >= len(messages):
raise ValueError(f"Target message index {idx} is out of bounds.")
# ``stream`` is enforced non-None at MessageTurn construction time
# (see ``MessageTurn.__post_init__``), so a missing stream here would
# mean the dataclass invariant was bypassed; no need to re-check.
def _nth_relative(
+42
View File
@@ -18,6 +18,7 @@ import dataclasses
import importlib.resources
import json
import logging
from collections.abc import Sequence
from dataclasses import dataclass, field
from pathlib import Path
@@ -98,6 +99,47 @@ VIDEO_DIR = "videos"
CHUNK_FILE_PATTERN = "chunk-{chunk_index:03d}/file-{file_index:03d}"
IMAGE_FILE_PATTERN = "frame-{frame_index:06d}.png"
def resolve_episode_indices(
episodes: Sequence[int] | None,
total_episodes: int,
exclude_episodes: Sequence[int] | None = None,
) -> list[int] | None:
"""Resolve an optional episode allowlist and exclusion list against dataset bounds.
``None`` is preserved when no filtering is requested so callers can retain
their native "all episodes" fast path. Invalid indices are ignored with a
warning, and the input order is preserved.
"""
if total_episodes < 0:
raise ValueError(f"total_episodes must be non-negative, got {total_episodes}")
if episodes is None and not exclude_episodes:
return None
candidates = list(range(total_episodes)) if episodes is None else list(episodes)
invalid = [episode for episode in candidates if not 0 <= episode < total_episodes]
if invalid:
logger.warning(
"Ignoring episode indices outside the dataset range [0, %d): %s",
total_episodes,
invalid,
)
candidates = [episode for episode in candidates if 0 <= episode < total_episodes]
excluded = set(exclude_episodes or [])
invalid_excluded = sorted(episode for episode in excluded if not 0 <= episode < total_episodes)
if invalid_excluded:
logger.warning(
"Ignoring excluded episode indices outside the dataset range [0, %d): %s",
total_episodes,
invalid_excluded,
)
excluded = {episode for episode in excluded if 0 <= episode < total_episodes}
return [episode for episode in candidates if episode not in excluded]
DEPTH_FILE_PATTERN = "frame-{frame_index:06d}.tiff"
DEFAULT_TASKS_PATH = "meta/tasks.parquet"
DEFAULT_EPISODES_PATH = EPISODES_DIR + "/" + CHUNK_FILE_PATTERN + ".parquet"
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Distributed-training runtime for LeRobot.
This package owns everything that turns the declarative topology in
:class:`lerobot.configs.parallelism.ParallelismConfig` into a running engine:
mesh math (:class:`~lerobot.distributed.parallel_dims.ParallelDims`), the
`Accelerator` factory (:func:`~lerobot.distributed.factory.make_accelerator`),
sharding-aware checkpoint helpers, and small rank utilities.
Setup-order contract (normative):
CP dispatch install -> activation checkpointing -> torch.compile ->
``fully_shard``/DDP (via ``accelerator.prepare``) -> optimizer rebind.
Only the last two steps are active today; CP/AC/compile are configured
placeholders wired in later rounds.
"""
from .factory import guard_against_env_interference, make_accelerator, set_fsdp_wrap_modules
from .parallel_dims import ParallelDims
from .utils import finalize_sharded_policy, is_main_process, strip_accelerate_cp_hooks
__all__ = [
"ParallelDims",
"finalize_sharded_policy",
"guard_against_env_interference",
"is_main_process",
"make_accelerator",
"set_fsdp_wrap_modules",
"strip_accelerate_cp_hooks",
]
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sharding-aware checkpoint primitives.
Two artifact channels with distinct owners:
- the **distributable** ``model.safetensors``: produced by ``PreTrainedPolicy.save_pretrained``
through :func:`full_model_state_dict` a collective full gather when the model is sharded;
- the **resume** channel (sharded runs): torch DCP directories written/read through accelerate's
``save/load_fsdp_model`` and ``save/load_fsdp_optimizer`` (``pytorch_model_fsdp_0/`` and
``optimizer_0/``, names imported from accelerate constants), which reshard on load across
topology changes.
Every function that touches sharded state is a collective and must run on ALL ranks.
"""
from pathlib import Path
from typing import TYPE_CHECKING
import torch
from torch import nn
if TYPE_CHECKING:
from accelerate import Accelerator
def is_sharded_module(module: nn.Module) -> bool:
"""True when `fully_shard` owns this module's parameters (FSDP2's in-place class swap).
Args:
module (nn.Module): The module to inspect (a torch.compile wrapper is looked through
via `_orig_mod`).
Returns:
bool: True when the module (or its compiled `_orig_mod`) is an `FSDPModule`.
"""
from torch.distributed.fsdp import FSDPModule
if isinstance(module, FSDPModule):
return True
# torch.compile wraps the sharded module; mirror accelerate's `_orig_mod` check.
orig_mod = getattr(module, "_orig_mod", None)
return orig_mod is not None and isinstance(orig_mod, FSDPModule)
def full_model_state_dict(module: nn.Module) -> dict[str, torch.Tensor]:
"""The module's full (unsharded) state dict, however its parameters are laid out.
Sharded modules gather through torch's DCP state-dict API: a COLLECTIVE that must run on
every rank; with ``cpu_offload=True`` the full dict materializes on the main rank only and
every other rank receives a literal ``{}`` (runtime-verified a
rank-0-gated call deadlocks). Plain modules return ``module.state_dict()`` on every rank.
Args:
module (nn.Module): The (possibly sharded) module to read the state dict from.
Returns:
dict[str, torch.Tensor]: The full state dict on the main rank only (``{}``
elsewhere) when the module is sharded, on every rank otherwise.
"""
if not is_sharded_module(module):
return module.state_dict()
from torch.distributed.checkpoint.state_dict import StateDictOptions, get_model_state_dict
return get_model_state_dict(module, options=StateDictOptions(full_state_dict=True, cpu_offload=True))
def _fsdp_plugin(accelerator: "Accelerator") -> object:
"""The accelerator's FSDP plugin, required by every DCP save/load helper below.
Args:
accelerator (Accelerator): The accelerator that prepared the sharded model.
Returns:
object: The FSDP plugin held by `accelerator.state`.
Raises:
RuntimeError: If the accelerator was not configured with an FSDP plugin.
"""
plugin = getattr(accelerator.state, "fsdp_plugin", None)
if plugin is None:
raise RuntimeError("Sharded checkpointing requires an FSDP-prepared Accelerator.")
return plugin
def save_sharded_model(accelerator: "Accelerator", model: nn.Module, output_dir: Path) -> None:
"""Write the DCP model shards (`pytorch_model_fsdp_0/`). Collective: call on all ranks.
Args:
accelerator (Accelerator): The accelerator that prepared the sharded model.
model (nn.Module): The prepared (sharded) model to save.
output_dir (Path): The directory the shard subdirectory is created in.
"""
from accelerate.utils import save_fsdp_model
# accelerate 1.14's DCP helpers do string containment checks on the path:
# always hand them str, never Path.
save_fsdp_model(_fsdp_plugin(accelerator), accelerator, model, str(output_dir))
def load_sharded_model(accelerator: "Accelerator", model: nn.Module, input_dir: Path) -> None:
"""Load DCP model shards into the prepared (sharded) model. Collective: call on all ranks.
Args:
accelerator (Accelerator): The accelerator that prepared the sharded model.
model (nn.Module): The prepared (sharded) model to load into.
input_dir (Path): The directory containing the `pytorch_model_fsdp_0/` shard
subdirectory.
"""
from accelerate.utils import load_fsdp_model
from accelerate.utils.constants import FSDP_MODEL_NAME
# Pass the exact shard directory: accelerate's load resolves it with a substring check
# ("pytorch_model_fsdp" in the path -> use as-is), which misfires on run paths that happen
# to contain the marker; the exact dir makes the check deterministic.
load_fsdp_model(_fsdp_plugin(accelerator), accelerator, model, str(input_dir / f"{FSDP_MODEL_NAME}_0"))
def save_sharded_optimizer(
accelerator: "Accelerator", optimizer: torch.optim.Optimizer, model: nn.Module, output_dir: Path
) -> None:
"""Write the DCP optimizer shards (`optimizer_0/`). Collective: call on all ranks.
Args:
accelerator (Accelerator): The accelerator that prepared the model and optimizer.
optimizer (torch.optim.Optimizer): The prepared optimizer to save the state from.
model (nn.Module): The prepared (sharded) model the optimizer state is keyed by.
output_dir (Path): The directory the shard subdirectory is created in.
"""
from accelerate.utils import save_fsdp_optimizer
save_fsdp_optimizer(_fsdp_plugin(accelerator), accelerator, optimizer, model, str(output_dir))
def load_sharded_optimizer(
accelerator: "Accelerator", optimizer: torch.optim.Optimizer, model: nn.Module, input_dir: Path
) -> None:
"""Load DCP optimizer shards into the prepared optimizer. Collective: call on all ranks.
Must run AFTER ``accelerator.prepare()``: FSDP2's prepare rebinds the optimizer's param
groups to sharded DTensors but never migrates ``optimizer.state`` the resharding load is
the only correct way to restore it.
Args:
accelerator (Accelerator): The accelerator that prepared the model and optimizer.
optimizer (torch.optim.Optimizer): The prepared optimizer to restore the state into.
model (nn.Module): The prepared (sharded) model the optimizer state is keyed by.
input_dir (Path): The directory containing the `optimizer_0/` shard subdirectory.
"""
from accelerate.utils import load_fsdp_optimizer
from accelerate.utils.constants import OPTIMIZER_NAME
# Exact shard directory for the same reason as load_sharded_model: accelerate's substring
# check ("optimizer" in the path) would misread e.g. --job_name=optimizer_sweep run paths.
load_fsdp_optimizer(
_fsdp_plugin(accelerator), accelerator, optimizer, model, str(input_dir / f"{OPTIMIZER_NAME}_0")
)
def dcp_to_safetensors(dcp_dir: Path, output_dir: Path, *, delete_dcp: bool = False) -> Path:
"""Merge a DCP shard directory into a single `model.safetensors` (offline, single process).
Thin wrapper over `accelerate.utils.merge_fsdp_weights`, which loads the shards without a
process group, writes safetensors directly, and when asked removes the merged shard
directory itself, only on the main process and only once the merge has succeeded.
Args:
dcp_dir (Path): The DCP shard directory to merge (e.g. `.../pytorch_model_fsdp_0`).
output_dir (Path): The directory the merged `model.safetensors` is written into.
delete_dcp (bool): Whether to remove the shard directory once it has been merged.
Defaults to False.
Returns:
Path: The written `model.safetensors` file's path.
"""
from accelerate.utils import merge_fsdp_weights
merge_fsdp_weights(
str(dcp_dir), str(output_dir), safe_serialization=True, remove_checkpoint_dir=delete_dcp
)
return output_dir / "model.safetensors"
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The `Accelerator` factory — the only place accelerate gets configured.
`torchrun` is the launcher; every accelerate parameter comes from `TrainPipelineConfig`
(`cfg.parallelism` + `cfg.accelerator`) so a run is reproducible from its `train_config.json`
alone. `accelerate launch` without a `--config_file` remains equivalent (it only sets rendezvous
env vars in that mode); the yaml flow is superseded.
"""
import os
from typing import TYPE_CHECKING
from lerobot.configs.parallelism import world_size_from_env
from lerobot.configs.train import TrainPipelineConfig
if TYPE_CHECKING:
from accelerate import Accelerator
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_ENV_VARS = (
"ACCELERATE_USE_FSDP",
"ACCELERATE_USE_PARALLELISM_CONFIG",
"ACCELERATE_GRADIENT_ACCUMULATION_STEPS",
)
_ENV_OVERRIDE = "LEROBOT_ALLOW_ACCELERATE_ENV"
def guard_against_env_interference() -> None:
"""Hard-error when accelerate-configuring env vars are set.
A silently env-overridden "reproducible" config is worse than a stop: users migrating from
the old `accelerate launch --config_file fsdp.yaml` flow get a precise error instead of a
config that lies. Set LEROBOT_ALLOW_ACCELERATE_ENV=1 to acknowledge and proceed.
Raises:
RuntimeError: If any accelerate-configuring environment variable is set and the
LEROBOT_ALLOW_ACCELERATE_ENV override is not.
"""
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)
)
if offending:
raise RuntimeError(
f"Accelerate-configuring environment variables are set: {', '.join(offending)}. "
"LeRobot manages accelerate exclusively through TrainPipelineConfig "
"(--parallelism.* / --accelerator.*); launch with plain torchrun and remove these "
"variables (the `accelerate launch --config_file` flow is superseded), or set "
f"{_ENV_OVERRIDE}=1 to acknowledge that they may override your config."
)
def make_accelerator(cfg: TrainPipelineConfig) -> "Accelerator":
"""Resolve the topology against the launched world and build the `Accelerator`.
Must run once per process, before any other component needs the device or the process
group (`Accelerator.__init__` initializes both and builds the device mesh).
Args:
cfg (TrainPipelineConfig): The full training config; `cfg.parallelism` is resolved in
place against the launched world size and `cfg.accelerator` builds the result.
Returns:
Accelerator: The configured accelerator, with device and process group initialized.
Raises:
ValueError: If `cfg.checkpoint_format` requires DCP but the topology resolved to a
non-sharded run.
"""
guard_against_env_interference()
cfg.parallelism.resolve(world_size_from_env())
# The parse-time format check ran against the declared degrees, where the dp_shard=-1
# sentinel counts as sharded; it may resolve to an unsharded run (e.g. -1 at world size 1).
# Re-check against the concrete degrees so the recorded format never lies about the
# artifacts a checkpoint will actually contain.
if cfg.checkpoint_format.wants_dcp and not cfg.parallelism.is_sharded:
raise ValueError(
f"checkpoint_format={cfg.checkpoint_format.value} requires a sharded run, but the "
f"topology resolved to a non-sharded one (dp_replicate={cfg.parallelism.dp_replicate}, "
f"dp_shard={cfg.parallelism.dp_shard}); non-sharded checkpoints are always safetensors."
)
return cfg.accelerator.build(
cfg.parallelism,
cpu=cfg.trainable_config.device == "cpu",
)
def set_fsdp_wrap_modules(accelerator: "Accelerator", policy: "PreTrainedPolicy") -> None:
"""Resolve the FSDP wrap-unit class names onto the plugin before `accelerator.prepare()`.
Resolution order: user override (`--accelerator.fsdp.wrap_modules`, already on the plugin)
-> the policy's `_fsdp_wrap_modules` declaration -> hard error. Root-only wrapping — the
silent default when no wrap source exists is never accepted: it quietly forfeits all
sharding memory savings.
No-op for the size-based policy (`--accelerator.fsdp.min_num_params`), which needs no class
names, and for non-sharded runs (no fsdp plugin).
Args:
accelerator (Accelerator): The accelerator whose FSDP plugin receives the wrap-unit
class names.
policy (PreTrainedPolicy): The trainable whose class may declare `_fsdp_wrap_modules`.
Raises:
ValueError: If sharded class-based wrapping is configured but neither a user override
nor a policy declaration supplies wrap-unit class names.
"""
plugin = getattr(accelerator.state, "fsdp_plugin", None)
if plugin is None or plugin.min_num_params:
return
if plugin.transformer_cls_names_to_wrap: # user override, set at build time
return
# getattr, not attribute access: non-policy trainables (no `_fsdp_wrap_modules` attribute)
# must reach the actionable error below, not an AttributeError.
declared = getattr(type(policy), "_fsdp_wrap_modules", None)
if not declared:
raise ValueError(
f"Policy '{type(policy).__name__}' declares no FSDP wrap units. Sharded training "
"requires wrap-unit class names: set --accelerator.fsdp.wrap_modules='[\"MyBlock\"]' "
"(or --accelerator.fsdp.min_num_params for a size-based policy), or declare "
"`_fsdp_wrap_modules` on the policy class."
)
plugin.transformer_cls_names_to_wrap = list(declared)
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runtime mesh math derived from the declarative :class:`ParallelismConfig`.
`ParallelDims` is the training script's single source of truth for topology-derived numbers
(data-parallel world size and rank, sample accounting inputs) and once the CP engine lands
the owner of LeRobot's private ``(dp_replicate, dp_shard, ring, ulysses)`` mesh. It is a runtime
object and is never serialized (the config it derives from is what lands in
``train_config.json``).
"""
from dataclasses import dataclass
import torch.distributed as dist
from lerobot.configs.parallelism import ParallelismConfig
@dataclass(frozen=True)
class ParallelDims:
"""Concrete parallelism degrees bound to a world size (canonical row-major rank layout)."""
dp_replicate: int
dp_shard: int
ring: int
ulysses: int
world_size: int
device_type: str
@classmethod
def from_config(cls, cfg: ParallelismConfig, world_size: int, device_type: str) -> "ParallelDims":
"""Bind a *resolved* config to the actual runtime world size (cross-checked here).
Args:
cfg (ParallelismConfig): The declarative topology, already resolved via
`ParallelismConfig.resolve(world_size)`.
world_size (int): The launched world size the declared degrees must multiply to.
device_type (str): The accelerator device type backing the mesh (e.g. "cuda").
Returns:
ParallelDims: The concrete parallelism degrees bound to this world.
Raises:
ValueError: If the config is unresolved (`dp_shard == -1`) or its degrees do not
multiply to `world_size`.
"""
total = cfg.dp_replicate * cfg.dp_shard * cfg.cp_size
if cfg.dp_shard == -1 or total != world_size:
raise ValueError(
f"ParallelismConfig is not resolved against this world: dp_replicate="
f"{cfg.dp_replicate} * dp_shard={cfg.dp_shard} * cp={cfg.cp_size} != "
f"world_size={world_size}. Call ParallelismConfig.resolve(world_size) first "
"(make_accelerator does this)."
)
return cls(
dp_replicate=cfg.dp_replicate,
dp_shard=cfg.dp_shard,
ring=cfg.context_parallel.ring_degree,
ulysses=cfg.context_parallel.ulysses_degree,
world_size=world_size,
device_type=device_type,
)
@property
def cp_size(self) -> int:
"""Total context-parallel degree (`ring * ulysses`)."""
return self.ring * self.ulysses
@property
def is_sharded(self) -> bool:
"""Whether parameters are sharded (`dp_shard > 1` or any context parallelism)."""
return self.dp_shard > 1 or self.cp_size > 1
@property
def dp_world_size(self) -> int:
"""Number of distinct data-parallel workers — the divisor for all sample accounting."""
return self.dp_replicate * self.dp_shard
@property
def dp_rank(self) -> int:
"""This process's data-parallel coordinate (CP peers share one dp_rank).
With the canonical row-major layout and (ring, ulysses) innermost, CP peers are
contiguous global ranks, so the dp coordinate is the integer quotient by cp_size
the same arithmetic accelerate's mesh-aware dataloader applies.
"""
global_rank = dist.get_rank() if dist.is_initialized() else 0
return global_rank // self.cp_size
def cp_mesh(self) -> None:
"""Private (ring, ulysses) mesh for the CP engine — reserved for the CP round.
Raises:
NotImplementedError: Always context parallelism is not implemented yet.
"""
raise NotImplementedError(
"Context parallelism is not implemented yet; ParallelDims.cp_mesh is reserved for "
"the CP engine round (a private mesh aligned with accelerate's cp block)."
)
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rank utilities and post-`prepare()` sharding finalization."""
import logging
from typing import TYPE_CHECKING
import torch.distributed as dist
from torch import nn
if TYPE_CHECKING:
from lerobot.distributed.parallel_dims import ParallelDims
def is_main_process() -> bool:
"""True on the process that owns rank-0-only side effects (file writes, uploads, logging).
Torch-native on purpose: persistence code must not depend on an `Accelerator` handle
`_save_pretrained` and the hub publishers run in contexts that have none. Outside
distributed runs every process is the main process.
Returns:
bool: True when this process is rank 0 or no process group is initialized.
"""
return not dist.is_initialized() or dist.get_rank() == 0
def strip_accelerate_cp_hooks(model: nn.Module) -> int:
"""Remove accelerate's context-parallel forward-pre-hooks from every module.
When `cp_size > 1` is declared, `accelerator.prepare()` unconditionally attaches hooks that
silently replace any `attention_mask` kwarg of `*self_attn` modules with `is_causal=True`
(`accelerate.big_modeling._attach_context_parallel_hooks`) mask corruption for policies
with non-causal attention. LeRobot implements CP itself and never enters accelerate's CP
context, so these hooks are pure hazard. Deterministically identified by their defining
module; a version canary pins that identity.
Args:
model (nn.Module): The prepared model to strip the hooks from (all submodules are
visited).
Returns:
int: The number of hooks removed.
"""
removed = 0
for module in model.modules():
for hook_id, hook in list(module._forward_pre_hooks.items()):
if getattr(hook, "__module__", None) == "accelerate.big_modeling":
del module._forward_pre_hooks[hook_id]
module._forward_pre_hooks_with_kwargs.pop(hook_id, None)
removed += 1
return removed
def finalize_sharded_policy(policy: nn.Module, parallel_dims: "ParallelDims") -> None:
"""Sharding correctness protocol, applied once, immediately after `accelerator.prepare()`.
1. Strip accelerate's CP mask hooks (only attached when cp > 1 was declared).
2. Register the policy's non-`forward` entry points (`_fsdp_forward_methods`) so FSDP2
unshards parameters around `select_action` & co. without this, any inference-style
call on a sharded policy crashes on mixed Tensor/DTensor.
No-op for DDP/single-process runs.
Args:
policy (nn.Module): The policy as returned by `accelerator.prepare()`.
parallel_dims (ParallelDims): The run's resolved topology; decides whether the protocol
applies.
"""
if not parallel_dims.is_sharded:
return
if parallel_dims.cp_size > 1:
removed = strip_accelerate_cp_hooks(policy)
logging.info("Stripped %d accelerate context-parallel attention-mask hooks.", removed)
from torch.distributed.fsdp import FSDPModule, register_fsdp_forward_method
if isinstance(policy, FSDPModule):
for method_name in getattr(type(policy), "_fsdp_forward_methods", ()):
if callable(getattr(policy, method_name, None)):
register_fsdp_forward_method(policy, method_name)
+1 -1
View File
@@ -432,7 +432,7 @@ def submit_to_hf(cfg: TrainPipelineConfig) -> None:
# Finish as soon as the model is pushed, rather than waiting out the platform's
# post-run finalization before the job stage flips to COMPLETED. This matches the
# exact log line emitted by PreTrainedPolicy.push_model_to_hub — the two must stay
# exact log line emitted by lerobot.common.train_utils.publish_trained_model — the two must stay
# in sync. If it ever stops matching we just fall back to stage-based completion
# (~30s slower), so the contract is an optimization, not a correctness requirement.
success_marker = f"Model pushed to https://huggingface.co/{repo_id}"
+24 -17
View File
@@ -314,11 +314,16 @@ class SerialMotorsBus(MotorsBusBase):
To find the port, you can run our utility script:
```bash
lerobot-find-port.py
>>> Finding all available ports for the MotorsBus.
>>> ["/dev/tty.usbmodem575E0032081", "/dev/tty.usbmodem575E0031751"]
>>> Remove the usb cable from your MotorsBus and press Enter when done.
>>> The port of this MotorsBus is /dev/tty.usbmodem575E0031751.
>>> Reconnect the usb cable.
```
which prints:
```
Finding all available ports for the MotorsBus.
["/dev/tty.usbmodem575E0032081", "/dev/tty.usbmodem575E0031751"]
Remove the usb cable from your MotorsBus and press Enter when done.
The port of this MotorsBus is /dev/tty.usbmodem575E0031751.
Reconnect the usb cable.
```
Example of usage for 1 Feetech sts3215 motor connected to the bus:
@@ -595,7 +600,7 @@ class SerialMotorsBus(MotorsBusBase):
ID, and finally programs the bus' default baud-rate.
Args:
motor (str): Key of the motor in :pyattr:`motors`.
motor (str): Key of the motor in `motors`.
initial_baudrate (int | None, optional): Current baud-rate (skips scanning when provided).
Defaults to None.
initial_id (int | None, optional): Current ID (skips scanning when provided). Defaults to None.
@@ -666,7 +671,7 @@ class SerialMotorsBus(MotorsBusBase):
"""Enable torque on selected motors.
Args:
motors (int | str | list[str] | None, optional): Same semantics as :pymeth:`disable_torque`.
motors (int | str | list[str] | None, optional): Same semantics as [`~motors.motors_bus.MotorsBus.disable_torque`].
Defaults to `None`.
num_retry (int, optional): Number of additional retry attempts on communication failure.
Defaults to 0.
@@ -679,10 +684,12 @@ class SerialMotorsBus(MotorsBusBase):
This helper is useful to temporarily disable torque when configuring motors.
Examples:
>>> with bus.torque_disabled():
Example:
```python
>>> with bus.torque_disabled(): # doctest: +SKIP
... # Safe operations here
... pass
```
"""
self.disable_torque(motors)
try:
@@ -695,7 +702,7 @@ class SerialMotorsBus(MotorsBusBase):
Args:
timeout_ms (int | None, optional): Timeout in *milliseconds*. If `None` (default) the method falls
back to :pyattr:`default_timeout`.
back to `default_timeout`.
"""
timeout_ms = timeout_ms if timeout_ms is not None else self.default_timeout
self.port_handler.setPacketTimeoutMillis(timeout_ms)
@@ -746,8 +753,8 @@ class SerialMotorsBus(MotorsBusBase):
Args:
calibration_dict (dict[str, MotorCalibration]): Calibration obtained from
:pymeth:`read_calibration` or crafted by the user.
cache (bool, optional): Save the calibration to :pyattr:`calibration`. Defaults to True.
[`~motors.motors_bus.MotorsBus.read_calibration`] or crafted by the user.
cache (bool, optional): Save the calibration to `calibration`. Defaults to True.
"""
pass
@@ -755,7 +762,7 @@ class SerialMotorsBus(MotorsBusBase):
"""Restore factory calibration for the selected motors.
Homing offset is set to ``0`` and min/max position limits are set to the full usable range.
The in-memory :pyattr:`calibration` is cleared.
The in-memory `calibration` is cleared.
Args:
motors (NameOrID | Sequence[NameOrID] | None, optional): Selection of motors. `None` (default)
@@ -1069,9 +1076,9 @@ class SerialMotorsBus(MotorsBusBase):
) -> None:
"""Write a value to a single motor's register.
Contrary to :pymeth:`sync_write`, this expects a response status packet emitted by the motor, which
Contrary to [`~motors.motors_bus.MotorsBus.sync_write`], this expects a response status packet emitted by the motor, which
provides a guarantee that the value was written to the register successfully. In consequence, it is
slower than :pymeth:`sync_write` but it is more reliable. It should typically be used when configuring
slower than [`~motors.motors_bus.MotorsBus.sync_write`] but it is more reliable. It should typically be used when configuring
motors.
Args:
@@ -1228,8 +1235,8 @@ class SerialMotorsBus(MotorsBusBase):
) -> None:
"""Write the same register on multiple motors.
Contrary to :pymeth:`write`, this *does not* expects a response status packet emitted by the motor, which
can allow for lost packets. It is faster than :pymeth:`write` and should typically be used when
Contrary to [`~motors.motors_bus.MotorsBus.write`], this *does not* expects a response status packet emitted by the motor, which
can allow for lost packets. It is faster than [`~motors.motors_bus.MotorsBus.write`] and should typically be used when
frequency matters and losing some packets is acceptable (e.g. teleoperation loops).
Args:
-2
View File
@@ -20,7 +20,6 @@ from .optimizers import (
SGDConfig as SGDConfig,
XVLAAdamWConfig as XVLAAdamWConfig,
load_optimizer_state,
load_optimizer_state_dict,
save_optimizer_state,
)
from .schedulers import (
@@ -51,7 +50,6 @@ __all__ = [
"VQBeTSchedulerConfig",
# State management
"load_optimizer_state",
"load_optimizer_state_dict",
"load_scheduler_state",
"save_optimizer_state",
"save_scheduler_state",
+14 -29
View File
@@ -27,7 +27,7 @@ from lerobot.utils.constants import (
OPTIMIZER_PARAM_GROUPS,
OPTIMIZER_STATE,
)
from lerobot.utils.io_utils import deserialize_json_into_object, load_json, write_json
from lerobot.utils.io_utils import deserialize_json_into_object, write_json
from lerobot.utils.utils import flatten_dict, unflatten_dict
# Type alias for parameters accepted by optimizer build() methods.
@@ -52,6 +52,11 @@ class OptimizerConfig(draccus.ChoiceRegistry, abc.ABC):
def type(self) -> str:
return self.get_choice_name(self.__class__)
@property
def builds_multiple_optimizers(self) -> bool:
"""True when build() returns a dict of optimizers (unsupported under sharded training)."""
return False
@classmethod
def default_choice_name(cls) -> str | None:
return "adam"
@@ -245,6 +250,10 @@ class MultiAdamConfig(OptimizerConfig):
grad_clip_norm: float = 10.0
optimizer_groups: dict[str, dict[str, Any]] = field(default_factory=dict)
@property
def builds_multiple_optimizers(self) -> bool:
return True
def build(self, params: OptimizerParams) -> dict[str, torch.optim.Optimizer]:
"""Build multiple Adam optimizers.
@@ -283,35 +292,27 @@ class MultiAdamConfig(OptimizerConfig):
def save_optimizer_state(
optimizer: torch.optim.Optimizer | dict[str, torch.optim.Optimizer],
save_dir: Path,
optim_state_dict: dict | None = None,
) -> None:
"""Save optimizer state to disk.
"""Save optimizer state to disk (non-sharded runs; sharded runs use the DCP channel).
Args:
optimizer: Either a single optimizer or a dictionary of optimizers.
save_dir: Directory to save the optimizer state.
optim_state_dict: Pre-gathered optimizer state dict (for FSDP, where the sharded state must
be gathered across ranks first). If provided, it is saved directly instead of calling
``optimizer.state_dict()``. Only supported for a single optimizer. Defaults to None.
"""
if isinstance(optimizer, dict):
# Handle dictionary of optimizers
if optim_state_dict is not None:
raise ValueError("optim_state_dict is not supported for a dict of optimizers")
for name, opt in optimizer.items():
optimizer_dir = save_dir / name
optimizer_dir.mkdir(exist_ok=True, parents=True)
_save_single_optimizer_state(opt, optimizer_dir)
else:
# Handle single optimizer
_save_single_optimizer_state(optimizer, save_dir, optim_state_dict=optim_state_dict)
_save_single_optimizer_state(optimizer, save_dir)
def _save_single_optimizer_state(
optimizer: torch.optim.Optimizer, save_dir: Path, optim_state_dict: dict | None = None
) -> None:
def _save_single_optimizer_state(optimizer: torch.optim.Optimizer, save_dir: Path) -> None:
"""Save a single optimizer's state to disk."""
state = dict(optim_state_dict) if optim_state_dict is not None else optimizer.state_dict()
state = optimizer.state_dict()
param_groups = state.pop("param_groups")
flat_state = flatten_dict(state)
save_file(flat_state, save_dir / OPTIMIZER_STATE)
@@ -365,19 +366,3 @@ def _load_single_optimizer_state(optimizer: torch.optim.Optimizer, save_dir: Pat
optimizer.load_state_dict(loaded_state_dict)
return optimizer
def load_optimizer_state_dict(save_dir: Path) -> dict:
"""Read a saved optimizer state dict (safetensors + json) back into a plain dict.
Unlike `load_optimizer_state`, this does not load into an optimizer and preserves the original
``state`` keys verbatim (e.g. FSDP parameter FQNs, which are not integer-castable). It is used by
the FSDP resume path, where the full state must be resharded via `FSDP.optim_state_dict_to_load`
before being loaded into the (sharded) optimizer.
"""
flat_state = load_file(save_dir / OPTIMIZER_STATE)
state = unflatten_dict(flat_state)
return {
"state": state.get("state", {}),
"param_groups": load_json(save_dir / OPTIMIZER_PARAM_GROUPS),
}
+92 -37
View File
@@ -40,44 +40,93 @@ class ACTConfig(PreTrainedConfig):
- "action" is required as an output key.
Args:
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
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
which is used as the VAE's encoder (not to be confused with the transformer encoder - see
documentation in the policy class).
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`.
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.
"""
# Input / output structure.
@@ -128,9 +177,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}."
@@ -151,26 +200,32 @@ 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
+38 -23
View File
@@ -40,23 +40,25 @@ 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)
"""
config_class = ACTConfig
name = "act"
# FSDP2 wrap units: one unit per transformer layer of both stacks.
_fsdp_wrap_modules = ["ACTEncoderLayer", "ACTDecoderLayer"]
def __init__(
self,
config: ACTConfig,
**kwargs,
):
"""
"""Build the ACT model (and, if enabled, the temporal ensembler) from `config`.
Args:
config: Policy configuration class instance or None, in which case the default instantiation of
the configuration class is used.
config (`ACTConfig`):
Policy configuration.
"""
super().__init__(config)
config.validate_features()
@@ -70,6 +72,11 @@ 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 [
@@ -91,7 +98,11 @@ class ACTPolicy(PreTrainedPolicy):
]
def reset(self):
"""This should be called whenever the environment is reset."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
Resets the `ACTTemporalEnsembler` when temporal ensembling is enabled, otherwise clears the action
queue consumed by `select_action`.
"""
if self.config.temporal_ensemble_coeff is not None:
self.temporal_ensembler.reset()
else:
@@ -99,11 +110,11 @@ class ACTPolicy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select a single action given environment observations.
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
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.
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`.
"""
self.eval() # keeping the policy in eval mode as it could be set to train mode while queue is consumed
@@ -124,7 +135,7 @@ class ACTPolicy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
"""Predict a chunk of actions given environment observations."""
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]."""
self.eval()
if self.config.image_features:
@@ -135,7 +146,11 @@ class ACTPolicy(PreTrainedPolicy):
return actions
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
"""Run the batch through the model and compute the loss for training or validation."""
"""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`.
"""
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]
@@ -219,8 +234,7 @@ 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)
@@ -624,13 +638,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.
"""
@@ -669,9 +683,11 @@ 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: Number of token positions required.
Returns: (num_positions, dimension) position embeddings (the first dimension is the batch dimension).
num_positions (`int`): Number of positions to embed (the sequence length).
dimension (`int`): The embedding dimension.
Returns:
`(num_positions, dimension)` position embeddings (the first dimension is the batch dimension).
"""
def get_position_angle_vec(position):
@@ -691,9 +707,8 @@ 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
@@ -703,9 +718,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.
"""
+1 -1
View File
@@ -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): A dictionary containing dataset
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): A dictionary containing dataset
statistics (e.g., mean and std) used for normalization. Defaults to None.
Returns:
@@ -41,63 +41,135 @@ class DiffusionConfig(PreTrainedConfig):
- "action" is required as an output key.
Args:
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
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
downsampling.
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
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
modulation.
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.
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.
"""
# Inputs / output structure.
@@ -164,9 +236,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}."
@@ -213,6 +285,7 @@ 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,
@@ -221,12 +294,14 @@ 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.")
@@ -249,12 +324,15 @@ 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,8 +54,7 @@ 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).
"""
@@ -67,12 +66,11 @@ class DiffusionPolicy(PreTrainedPolicy):
config: DiffusionConfig,
**kwargs,
):
"""
"""Build the diffusion model from `config`.
Args:
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.
config (`DiffusionConfig`):
Policy configuration.
"""
require_package("diffusers", extra="diffusion")
super().__init__(config)
@@ -87,10 +85,14 @@ 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):
"""Clear observation and action queues. Should be called on `env.reset()`"""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
Clears the observation and action queues consumed by `select_action`.
"""
self._queues = {
OBS_STATE: deque(maxlen=self.config.n_obs_steps),
ACTION: deque(maxlen=self.config.n_action_steps),
@@ -102,7 +104,7 @@ class DiffusionPolicy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
"""Predict a chunk of actions given environment observations.
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
Supports two modes:
- Online (queues populated via select_action): stacks observations from internal queues.
@@ -123,7 +125,7 @@ class DiffusionPolicy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
"""Select a single action given environment observations.
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
This method handles caching a history of observations and an action trajectory generated by the
underlying diffusion model. Here's how it works:
@@ -161,7 +163,7 @@ class DiffusionPolicy(PreTrainedPolicy):
return action
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, None]:
"""Run the batch through the model and compute the loss for training or validation."""
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]."""
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:
@@ -174,8 +176,7 @@ 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")
@@ -306,8 +307,7 @@ 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,8 +333,7 @@ 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)
@@ -401,8 +400,7 @@ 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"
@@ -424,10 +422,9 @@ 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__()
@@ -450,9 +447,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.
"""
@@ -536,9 +533,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.
"""
@@ -562,11 +559,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.
"""
@@ -708,12 +705,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.
"""
@@ -798,10 +795,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,8 +34,7 @@ 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.
@@ -48,10 +47,8 @@ def make_diffusion_pre_post_processors(
2. Unnormalizing the output features to their original scale.
Args:
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.
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.
Returns:
A tuple containing the configured pre-processor and post-processor pipelines.
+120 -1
View File
@@ -42,7 +42,117 @@ else:
@PreTrainedConfig.register_subclass("eo1")
@dataclass
class EO1Config(PreTrainedConfig):
"""Configuration for native EO1 policy integration in LeRobot."""
"""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.
"""
vlm_base: str = "Qwen/Qwen2.5-VL-3B-Instruct"
vlm_config: dict | None = None
@@ -112,6 +222,7 @@ 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:
@@ -126,6 +237,7 @@ 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:
@@ -134,10 +246,12 @@ 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:
@@ -164,6 +278,7 @@ 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,
@@ -173,6 +288,7 @@ 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,
@@ -182,12 +298,15 @@ 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
+23 -1
View File
@@ -54,6 +54,14 @@ 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()
@@ -80,6 +88,7 @@ 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
@@ -87,6 +96,11 @@ 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})
@@ -97,6 +111,11 @@ 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])
@@ -107,13 +126,16 @@ 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:
@@ -123,6 +145,7 @@ 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()
@@ -358,7 +381,6 @@ 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,6 +31,155 @@ 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
@@ -108,6 +257,7 @@ 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(
@@ -200,6 +350,7 @@ 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:
@@ -226,6 +377,7 @@ 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,
@@ -235,18 +387,22 @@ 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,19 +33,43 @@ 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()
@@ -93,6 +117,12 @@ 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)
@@ -170,6 +200,11 @@ 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:
@@ -186,6 +221,7 @@ 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]:
@@ -362,6 +398,12 @@ 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
@@ -452,6 +494,12 @@ 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)
@@ -486,6 +534,12 @@ 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")
@@ -522,6 +576,11 @@ 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,6 +381,25 @@ 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)
+52 -37
View File
@@ -77,8 +77,7 @@ 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
@@ -88,7 +87,8 @@ def get_policy_class(name: str) -> type[PreTrainedPolicy]:
``@PreTrainedConfig.register_subclass``).
Args:
name: The registered name of the policy (e.g. "act", "diffusion", "pi0").
name (`str`): The registered name of the policy (e.g. "act", "diffusion", "pi0").
Returns:
The policy class corresponding to the given name.
@@ -100,16 +100,15 @@ 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: 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.
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.
Returns:
An instance of a `PreTrainedConfig` subclass.
@@ -125,18 +124,21 @@ 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.
Attributes:
preprocessor_config_filename: The filename for the preprocessor configuration.
postprocessor_config_filename: The filename for the postprocessor configuration.
preprocessor_overrides: A dictionary of overrides for the preprocessor configuration.
postprocessor_overrides: A dictionary of overrides for the postprocessor configuration.
dataset_stats: Dataset statistics for normalization.
**Attributes**:
- **preprocessor_config_filename** (`str | None`) -- The filename for the preprocessor configuration.
- **postprocessor_config_filename** (`str | None`) -- The filename for the postprocessor
configuration.
- **preprocessor_overrides** (`dict[str, Any] | None`) -- A dictionary of overrides for the
preprocessor configuration.
- **postprocessor_overrides** (`dict[str, Any] | None`) -- A dictionary of overrides for the
postprocessor configuration.
- **dataset_stats** (`dict[str, dict[str, torch.Tensor]] | None`) -- Dataset statistics for
normalization.
"""
preprocessor_config_filename: str | None
@@ -156,8 +158,7 @@ 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
@@ -168,6 +169,7 @@ 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`.
@@ -242,9 +244,9 @@ def make_policy(
ds_meta: LeRobotDatasetMetadata | None = None,
env_cfg: EnvConfig | None = None,
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
@@ -252,22 +254,27 @@ def make_policy(
can either initialize a new policy from scratch or load a pretrained one.
Args:
cfg: 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: Dataset metadata used to infer feature shapes and types. Also provides
statistics for normalization layers.
env_cfg: Environment configuration used to infer feature shapes and types.
One of `ds_meta` or `env_cfg` must be provided.
rename_map: Optional mapping of dataset or environment feature keys to match
expected policy feature names (e.g., `"left"` `"camera1"`).
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
types. Also provides statistics for normalization layers.
env_cfg (EnvConfig | None, *optional*): 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
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
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
engine overwrites the random init).
Returns:
An instantiated and device-placed policy model.
PreTrainedPolicy: An instantiated and device-placed policy model.
Raises:
ValueError: If both or neither of `ds_meta` and `env_cfg` are provided.
NotImplementedError: If attempting to use an unsupported policy-backend
combination (e.g., VQBeT with 'mps').
NotImplementedError: If attempting to use an unsupported policy-backend combination
(e.g., VQBeT with 'mps').
"""
if bool(ds_meta) == bool(env_cfg):
raise ValueError("Either one of a dataset metadata or a sim env must be provided.")
@@ -332,11 +339,18 @@ def make_policy(
)
if cfg.pretrained_path and not cfg.use_peft:
# Load a pretrained policy and override the config if needed (for example, if there are inference-time
# hyperparameters that we want to vary).
kwargs["pretrained_name_or_path"] = cfg.pretrained_path
kwargs["revision"] = cfg.pretrained_revision
policy = policy_cls.from_pretrained(**kwargs)
if defer_weight_load:
# Same construction path as from_pretrained (config already resolved from the
# checkpoint by the caller; dataset_stats/dataset_meta kwargs identical), minus the
# weight load — parity by construction.
policy = policy_cls(**kwargs)
policy.eval()
else:
# Load a pretrained policy and override the config if needed (for example, if there
# are inference-time hyperparameters that we want to vary).
kwargs["pretrained_name_or_path"] = cfg.pretrained_path
kwargs["revision"] = cfg.pretrained_revision
policy = policy_cls.from_pretrained(**kwargs)
elif cfg.pretrained_path and cfg.use_peft:
# Load a pretrained PEFT model on top of the policy. The pretrained path points to the folder/repo
# of the adapter and the adapter's config contains the path to the base policy. So we need the
@@ -395,6 +409,7 @@ 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.
"""
@@ -450,10 +465,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,6 +58,7 @@ _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,
@@ -81,6 +82,7 @@ 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,
@@ -136,7 +138,6 @@ 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(
@@ -153,30 +154,129 @@ 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:
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`, *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.
"""
n_obs_steps: int = 1
@@ -232,6 +332,7 @@ 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")
@@ -280,9 +381,11 @@ 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:
@@ -317,6 +420,7 @@ 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:
@@ -377,12 +481,16 @@ class FastWAMConfig(PreTrainedConfig):
@property
def model_video_frames(self) -> int:
"""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)."""
"""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).
"""
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
@@ -392,8 +500,10 @@ 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,15 +45,13 @@ 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
name = "fastwam"
# FSDP2 wrap units: MoTLayer is the single FSDP owner of each layer's expert blocks
# (the blocks are re-parented onto it precisely so sharding has one boundary to hook).
_fsdp_wrap_modules = ["MoTLayer"]
def __init__(
self,
@@ -61,6 +59,17 @@ 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.
@@ -137,6 +146,12 @@ 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".
@@ -149,6 +164,7 @@ 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]:
@@ -184,36 +200,24 @@ class FastWAMPolicy(PreTrainedPolicy):
return sample
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict[str, Any]]:
"""Compute FastWAM training loss for a LeRobot batch.
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
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.
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`.
"""
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:
"""Predict a chunk of actions from the current FastWAM observation.
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
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]`.
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]`.
"""
self.eval()
infer_kwargs = _batch_to_infer_kwargs(batch=batch, config=self.config)
batch_size = _infer_kwargs_batch_size(infer_kwargs)
@@ -235,6 +239,7 @@ 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,14 +73,13 @@ 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
dataset_stats (dict[str, dict[str, torch.Tensor]] | None, *optional*): 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: The feature key to check
key (`str`): The feature key to check.
Returns:
True if the key represents an image feature, False otherwise
@@ -54,6 +54,8 @@ 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
@@ -62,6 +64,8 @@ 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
@@ -69,12 +73,16 @@ 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
@@ -94,9 +102,95 @@ 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,
@@ -106,7 +200,6 @@ 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: {
@@ -125,60 +218,42 @@ 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
@@ -195,9 +270,11 @@ 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
@@ -211,16 +288,20 @@ 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,6 +35,14 @@ 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"
@@ -42,6 +50,11 @@ 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
@@ -53,6 +66,12 @@ 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
@@ -63,20 +82,30 @@ class GaussianActorPolicy(
return optim_params
def reset(self):
"""Reset the policy"""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. This policy holds no episode-scoped state,
so this is a no-op.
"""
pass
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
"""Predict a chunk of actions given environment observations."""
"""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`.
"""
raise NotImplementedError(
"GaussianActorPolicy does not support action chunking. It returns single actions!"
)
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select action for inference/evaluation"""
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
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)
@@ -96,15 +125,19 @@ class GaussianActorPolicy(
return actions
def forward(self, batch: dict[str, Tensor | dict[str, Tensor]]) -> dict[str, Tensor]:
"""Actor forward pass: sample actions and return log-probabilities.
"""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`).
Args:
batch: A flat observation dict, or a training dict containing
``"state"`` (observations) and optionally ``"observation_feature"``
batch (dict[str, Tensor | dict[str, Tensor]]): A flat observation dict, or a training dict
containing `"state"` (observations) and optionally `"observation_feature"`
(pre-computed encoder features).
Returns:
Dict with ``"action"``, ``"log_prob"``, and ``"action_mean"`` tensors.
dict[str, Tensor]: 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
@@ -311,10 +344,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): 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.
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.
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.
@@ -562,8 +595,7 @@ 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
@@ -582,8 +614,7 @@ 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,
@@ -591,7 +622,6 @@ 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,8 +35,7 @@ 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.
@@ -49,8 +48,8 @@ def make_gaussian_actor_pre_post_processors(
2. Unnormalizing the output features to their original scale.
Args:
config: The configuration object for the tanh-Gaussian policy.
dataset_stats: A dictionary of statistics for normalization.
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.
Returns:
A tuple containing the configured pre-processor and post-processor pipelines.
@@ -74,6 +74,11 @@ _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
@@ -85,6 +90,11 @@ 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())
@@ -100,6 +110,7 @@ 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()
@@ -117,6 +128,7 @@ 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
@@ -133,6 +145,7 @@ 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
@@ -152,6 +165,13 @@ 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
@@ -185,6 +205,13 @@ 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
@@ -241,7 +268,127 @@ def _infer_groot_model_version_from_config(config: dict) -> str | None:
@PreTrainedConfig.register_subclass("groot")
@dataclass
class GrootConfig(PreTrainedConfig):
"""Configuration for Groot policy wrapper."""
"""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.
"""
# Basic policy settings
n_obs_steps: int = 1
@@ -372,6 +519,12 @@ 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 "
+57 -27
View File
@@ -14,8 +14,7 @@
# 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
@@ -69,10 +68,17 @@ 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):
"""Initialize Groot policy wrapper."""
"""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`.
"""
require_package("transformers", extra="groot")
super().__init__(config)
config.validate_features()
@@ -149,7 +155,7 @@ class GrootPolicy(PreTrainedPolicy):
]
def reset(self):
"""Reset policy state when environment resets."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the action queue."""
self._action_queue = deque([], maxlen=self._action_queue_steps)
@classmethod
@@ -168,27 +174,40 @@ class GrootPolicy(PreTrainedPolicy):
strict: bool = True,
**kwargs,
) -> T:
"""Load Groot policy from pretrained model.
"""Load a Groot policy from either a raw N1.7 checkpoint or a fine-tuned LeRobot checkpoint.
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: 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)
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.
Returns:
Initialized GrootPolicy instance with loaded model
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.
"""
requested_version = infer_groot_model_version(str(pretrained_name_or_path)) or GROOT_N1_7
logger.info(
@@ -285,7 +304,11 @@ class GrootPolicy(PreTrainedPolicy):
return policy
def get_optim_params(self): # type: ignore[override]
"""Isaac-GR00T excludes biases and normalization parameters from weight decay."""
"""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.
"""
return self._build_weight_decay_parameter_groups(self)
def _resolve_action_queue_steps(self) -> int:
@@ -307,7 +330,6 @@ 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,
@@ -444,9 +466,10 @@ class GrootPolicy(PreTrainedPolicy):
return inputs, options
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict]:
"""Training forward pass.
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
Delegates to Isaac-GR00T model.forward when inputs are compatible.
Delegates to the underlying Isaac-GR00T model's `forward`, run under BF16 autocast when
`config.use_bf16` is set.
"""
groot_inputs = self._filter_groot_inputs(batch, include_action=True)
@@ -472,12 +495,11 @@ class GrootPolicy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: object) -> Tensor:
"""Predict a chunk of actions for inference by delegating to Isaac-GR00T.
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
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.
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.
"""
self.eval()
@@ -513,7 +535,15 @@ class GrootPolicy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select single action from action queue."""
"""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.
"""
if getattr(self.config, "use_relative_actions", False):
raise NotImplementedError(
"GrootPolicy.select_action does not support relative-action policies because cached "
+29 -34
View File
@@ -56,7 +56,6 @@ from lerobot.processor import (
AddBatchDimensionProcessorStep,
DeviceProcessorStep,
PolicyAction,
PolicyActionProcessorStep,
PolicyProcessorPipeline,
ProcessorStep,
ProcessorStepRegistry,
@@ -166,7 +165,6 @@ 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
@@ -274,7 +272,6 @@ 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)
@@ -382,7 +379,6 @@ _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
@@ -415,7 +411,6 @@ 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
@@ -488,7 +483,6 @@ 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.
@@ -585,7 +579,6 @@ 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,
@@ -1156,13 +1149,13 @@ def make_groot_pre_post_processors(
This mirrors SO100-style preprocessing and keeps scales consistent with GR00T.
Args:
config: Groot configuration containing data_config, embodiment_tag, etc.
dataset_stats: Optional per-key min/max statistics for normalization before padding.
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`.
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
@@ -1355,7 +1348,6 @@ 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]
@@ -2011,7 +2003,6 @@ 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]:
@@ -2226,7 +2217,6 @@ 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, {})
@@ -2298,7 +2288,7 @@ def _apply_n1_7_action_decode_transform(
@dataclass
@ProcessorStepRegistry.register(name="groot_n1_7_action_decode_v1")
class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
class GrootN17ActionDecodeStep(ProcessorStep):
"""Decode the full 132-D N1.7 model action back to environment actions.
N1.7 predicts checkpoint-order action groups. This step unnormalizes each
@@ -2319,8 +2309,6 @@ class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
and chunk index alongside each queued action through the postprocessor.
"""
skip_if_missing = True
env_action_dim: int = 0
raw_stats: dict[str, Any] | None = None
modality_config: dict[str, Any] | None = None
@@ -2329,17 +2317,20 @@ class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
action_decode_transform: str | None = None
pack_step: GrootN17PackInputsStep | None = field(default=None, repr=False)
def action(self, action: PolicyAction) -> PolicyAction:
def __call__(self, transition: EnvTransition) -> EnvTransition:
action = transition.get(TransitionKey.ACTION)
if not isinstance(action, torch.Tensor):
return transition
if self.raw_stats is None or self.modality_config is None:
return action
return transition
action_config = self.modality_config.get("action", {})
if not isinstance(action_config, dict):
return action
return transition
action_keys = action_config.get("modality_keys", [])
action_configs = action_config.get("action_configs", [])
if not isinstance(action_keys, list) or not isinstance(action_configs, list):
return action
return transition
action_np = action.detach().cpu().float().numpy()
if self.use_relative_action and action_np.ndim != 3:
@@ -2420,7 +2411,7 @@ class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
raise ValueError(f"Unsupported relative N1.7 action config for '{key}': {cfg}")
if not decoded_groups:
return action
return transition
decoded = np.concatenate(
[decoded_groups[key] for key in action_keys if isinstance(key, str) and key in decoded_groups],
@@ -2436,7 +2427,11 @@ class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
)
if squeeze_horizon:
decoded = decoded[:, 0]
return torch.as_tensor(decoded, dtype=action.dtype, device=action.device)
new_transition = transition.copy()
new_transition[TransitionKey.ACTION] = torch.as_tensor(
decoded, dtype=action.dtype, device=action.device
)
return new_transition
def transform_features(self, features):
return features
@@ -2457,9 +2452,7 @@ class GrootN17ActionDecodeStep(PolicyActionProcessorStep):
# silently load into it (v1 is stubbed below with the removal guidance).
@dataclass
@ProcessorStepRegistry.register(name="groot_action_unpack_unnormalize_v2")
class GrootActionUnpackUnnormalizeStep(PolicyActionProcessorStep):
skip_if_missing = True
class GrootActionUnpackUnnormalizeStep(ProcessorStep):
env_action_dim: int = 0
# Apply inverse of min-max normalization if it was used in preprocessor
normalize_min_max: bool = True
@@ -2468,8 +2461,12 @@ class GrootActionUnpackUnnormalizeStep(PolicyActionProcessorStep):
libero_gripper_action: bool = False
libero_gripper_binarize: bool = True
def action(self, action: PolicyAction) -> PolicyAction:
# Model outputs arrive as (B, T, D_model).
def __call__(self, transition: EnvTransition) -> EnvTransition:
# Expect model outputs to be in TransitionKey.ACTION as (B, T, D_model)
action = transition.get(TransitionKey.ACTION)
if not isinstance(action, torch.Tensor):
return transition
# Slice to env dimension while preserving an optional action horizon.
# Sync rollout postprocesses selected actions as (B, D); RTC postprocesses
# chunks as (B, T, D), matching Isaac-GR00T's decode_action contract.
@@ -2511,14 +2508,14 @@ class GrootActionUnpackUnnormalizeStep(PolicyActionProcessorStep):
action = action.clone()
action[..., -1] = gripper
return action
transition[TransitionKey.ACTION] = action
return transition
def transform_features(self, features):
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().
"""
@@ -2531,8 +2528,7 @@ class GrootActionUnpackUnnormalizeStep(PolicyActionProcessorStep):
}
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.
"""
@@ -2547,8 +2543,7 @@ class GrootActionUnpackUnnormalizeStep(PolicyActionProcessorStep):
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,7 +35,103 @@ 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."""
"""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.
"""
# Wan transformer architecture
patch_size: tuple[int, int, int] = (1, 2, 2)
@@ -114,6 +210,11 @@ 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}")
@@ -129,6 +230,11 @@ 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(
@@ -141,6 +247,7 @@ 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,
@@ -150,19 +257,23 @@ 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,6 +66,17 @@ 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)
@@ -146,12 +157,18 @@ class LingBotVAPolicy(PreTrainedPolicy):
# PreTrainedPolicy API
def get_optim_params(self) -> dict:
# 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.
"""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.
"""
return [p for p in self.transformer.parameters() if p.requires_grad]
def reset(self):
"""Reset all per-episode streaming state (KV cache, queues, frame counter)."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
Resets 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
@@ -323,11 +340,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]:
"""Training forward: dual-stream flow-matching loss.
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
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)
@@ -401,12 +418,14 @@ class LingBotVAPolicy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
"""Return one action, refilling the chunk (and feeding back observed keyframes) as needed.
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`].
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.
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.
"""
self.eval()
self._ensure_frozen_modules()
@@ -437,7 +456,11 @@ class LingBotVAPolicy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
"""Run one autoregressive chunk and return actions ``[B, chunk_size, n_used]`` (normalized)."""
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`].
Runs one autoregressive chunk and returns actions of shape `[B, chunk_size, n_used]`
(normalized).
"""
self.eval()
self._ensure_frozen_modules()
self._maybe_init_prompt(batch)
@@ -32,7 +32,149 @@ from ..rtc.configuration_rtc import RTCConfig
@PreTrainedConfig.register_subclass("molmoact2")
@dataclass
class MolmoAct2Config(PreTrainedConfig):
"""MolmoAct2 policy backed by the converted HF checkpoint implementation."""
"""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.
"""
checkpoint_path: str = "allenai/MolmoAct2"
checkpoint_revision: str | None = None
@@ -131,6 +273,13 @@ 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.")
@@ -199,17 +348,21 @@ 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,
@@ -219,6 +372,7 @@ 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,
@@ -227,6 +381,12 @@ 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,6 +521,10 @@ 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__(
@@ -531,6 +535,16 @@ 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()
@@ -610,7 +624,10 @@ class MolmoAct2Policy(PreTrainedPolicy):
self.train(self.training)
def reset(self) -> None:
"""Clear the action queue and rollout generator between episodes."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`].
Clears the action queue and the rollout-local action generator.
"""
self._action_queue = deque(maxlen=self.config.n_action_steps)
self._rollout_action_generator = None
@@ -634,6 +651,7 @@ 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)
@@ -683,6 +701,17 @@ 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()
@@ -719,7 +748,11 @@ class MolmoAct2Policy(PreTrainedPolicy):
param.requires_grad = False
def get_optim_params(self) -> list[dict[str, Any]]:
"""Return optimizer param groups with per-component learning rates."""
"""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.
"""
vit_params: list[Tensor] = []
connector_params: list[Tensor] = []
action_expert_params: list[Tensor] = []
@@ -1578,7 +1611,11 @@ class MolmoAct2Policy(PreTrainedPolicy):
batch: dict[str, Tensor],
reduction: str = "mean",
) -> tuple[Tensor, dict[str, Any]]:
"""Compute training loss (flow-matching and/or discrete token loss)."""
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
Computes the flow-matching loss, the discrete cross-entropy loss, or their sum, depending on
`config.action_mode`.
"""
if reduction not in {"mean", "none"}:
raise ValueError(f"Unsupported reduction={reduction!r}. Expected 'mean' or 'none'.")
model_inputs = self._model_inputs(batch)
@@ -1638,7 +1675,12 @@ class MolmoAct2Policy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
"""Generate an action chunk via continuous flow matching or discrete AR decoding."""
"""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.
"""
if "action_mode" in kwargs:
raise TypeError(
"MolmoAct2 predict_action_chunk got unexpected keyword argument 'action_mode'; "
@@ -1693,7 +1735,14 @@ class MolmoAct2Policy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor], **kwargs) -> Tensor:
"""Pop one action step from the queue, regenerating the chunk when empty."""
"""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`.
"""
if self._rtc_enabled():
raise AssertionError("RTC is not supported for select_action, use it with predict_action_chunk")
self.eval()
@@ -41,9 +41,7 @@ from lerobot.processor import (
AddBatchDimensionProcessorStep,
DeviceProcessorStep,
NormalizerProcessorStep,
ObservationProcessorStep,
PolicyAction,
PolicyActionProcessorStep,
PolicyProcessorPipeline,
ProcessorStep,
ProcessorStepRegistry,
@@ -1009,7 +1007,7 @@ class MolmoAct2PackInputsProcessorStep(ProcessorStep):
@ProcessorStepRegistry.register(name="molmoact2_state_frame_transform")
@dataclass
class MolmoAct2StateFrameTransformStep(ObservationProcessorStep):
class MolmoAct2StateFrameTransformStep(ProcessorStep):
"""Convert robot state from arm frame to model frame before normalization.
Required for zero-shot deployment of MolmoAct2-SO100_101 on SO-100/101
@@ -1025,21 +1023,25 @@ class MolmoAct2StateFrameTransformStep(ObservationProcessorStep):
See: https://huggingface.co/docs/lerobot/backwardcomp
"""
skip_if_missing = True
joint_signs: list[float] | None = None
joint_offsets: list[float] | None = None
def observation(self, observation: dict[str, Any]) -> dict[str, Any]:
if self.joint_signs is None or self.joint_offsets is None or OBS_STATE not in observation:
return observation
def __call__(self, transition: EnvTransition) -> EnvTransition:
if self.joint_signs is None or self.joint_offsets is None:
return transition
observation = transition.get(TransitionKey.OBSERVATION)
if not isinstance(observation, dict) or OBS_STATE not in observation:
return transition
transition = transition.copy()
observation = observation.copy()
state = torch.as_tensor(observation[OBS_STATE], dtype=torch.float32).clone()
n = len(self.joint_signs)
signs = torch.tensor(self.joint_signs, dtype=torch.float32, device=state.device)
offsets = torch.tensor(self.joint_offsets, dtype=torch.float32, device=state.device)
state[..., :n] = signs * state[..., :n] + offsets
observation[OBS_STATE] = state
return observation
transition[TransitionKey.OBSERVATION] = observation
return transition
def transform_features(
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
@@ -1052,7 +1054,7 @@ class MolmoAct2StateFrameTransformStep(ObservationProcessorStep):
@ProcessorStepRegistry.register(name="molmoact2_action_frame_transform")
@dataclass
class MolmoAct2ActionFrameTransformStep(PolicyActionProcessorStep):
class MolmoAct2ActionFrameTransformStep(ProcessorStep):
"""Convert model action from model frame back to arm frame after unnormalization.
Inverse of MolmoAct2StateFrameTransformStep. Required for zero-shot
@@ -1063,20 +1065,23 @@ class MolmoAct2ActionFrameTransformStep(PolicyActionProcessorStep):
See: https://huggingface.co/docs/lerobot/backwardcomp
"""
skip_if_missing = True
joint_signs: list[float] | None = None
joint_offsets: list[float] | None = None
def action(self, action: PolicyAction) -> PolicyAction:
def __call__(self, transition: EnvTransition) -> EnvTransition:
if self.joint_signs is None or self.joint_offsets is None:
return action
return transition
action = transition.get(TransitionKey.ACTION)
if action is None:
return transition
transition = transition.copy()
action = torch.as_tensor(action, dtype=torch.float32).clone()
n = len(self.joint_signs)
signs = torch.tensor(self.joint_signs, dtype=torch.float32, device=action.device)
offsets = torch.tensor(self.joint_offsets, dtype=torch.float32, device=action.device)
action[..., :n] = signs * (action[..., :n] - offsets)
return action
transition[TransitionKey.ACTION] = action
return transition
def transform_features(
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
@@ -1089,11 +1094,13 @@ class MolmoAct2ActionFrameTransformStep(PolicyActionProcessorStep):
@ProcessorStepRegistry.register(name="molmoact2_clamp_action")
@dataclass
class MolmoAct2ClampActionProcessorStep(PolicyActionProcessorStep):
skip_if_missing = True
def action(self, action: PolicyAction) -> PolicyAction:
return action.clamp(-1.0, 1.0)
class MolmoAct2ClampActionProcessorStep(ProcessorStep):
def __call__(self, transition: EnvTransition) -> EnvTransition:
transition = transition.copy()
action = transition.get(TransitionKey.ACTION)
if action is not None:
transition[TransitionKey.ACTION] = torch.as_tensor(action).clamp(-1.0, 1.0)
return transition
def transform_features(
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
@@ -1109,6 +1116,27 @@ 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,6 +28,141 @@ 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
@@ -105,6 +240,7 @@ 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:
@@ -189,6 +325,7 @@ 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,
@@ -197,6 +334,7 @@ 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,
@@ -235,20 +373,25 @@ 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,10 +68,21 @@ 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)
@@ -107,7 +118,11 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
self.reset()
def get_optim_params(self) -> list:
"""Returns parameter groups with different learning rates for vision vs non-vision parameters"""
"""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`.
"""
non_vision_params = []
vision_encoder_params = []
@@ -141,7 +156,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
return actions
def reset(self):
"""Clear observation and action queues. Should be called on `env.reset()`"""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the observation and action queues used by `select_action`."""
self._queues = {
OBS_STATE: deque(maxlen=self.config.n_obs_steps),
ACTION: deque(maxlen=self.config.n_action_steps),
@@ -152,7 +167,11 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor:
"""Predict a chunk of actions given environment observations"""
"""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"`).
"""
self.eval()
for k in batch:
@@ -172,7 +191,7 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select a single action given environment observations"""
"""See [`~policies.pretrained.PreTrainedPolicy.select_action`]. Uses an action queue populated by `predict_action_chunk`."""
if ACTION in batch:
batch = dict(batch) # shallow copy to avoid modifying original
batch.pop(ACTION)
@@ -189,7 +208,10 @@ class MultiTaskDiTPolicy(PreTrainedPolicy):
return action
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
"""Run the batch through the model and compute the loss for training"""
"""See [`~policies.pretrained.PreTrainedPolicy.forward`].
Computes the diffusion or flow-matching regression loss, depending on `config.objective`.
"""
batch = self._prepare_batch(batch)
conditioning_vec = self.observation_encoder.encode(batch)
@@ -36,8 +36,7 @@ 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.
@@ -51,15 +50,12 @@ def make_multi_task_dit_pre_post_processors(
2. Moving the data to the CPU.
Args:
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.
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.
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,6 +28,127 @@ 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"
@@ -103,6 +224,7 @@ 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
@@ -145,6 +267,7 @@ 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,
@@ -154,6 +277,7 @@ 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,
@@ -163,12 +287,15 @@ 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
+37 -14
View File
@@ -744,12 +744,17 @@ class PI0Pytorch(nn.Module): # see openpi `PI0Pytorch`
class PI0Policy(PreTrainedPolicy):
"""PI0 OpenPI Policy for LeRobot."""
"""PyTorch port of Physical Intelligence's PI0 vision-language-action policy, generating action
chunks via flow matching.
"""
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__(
@@ -757,9 +762,10 @@ class PI0Policy(PreTrainedPolicy):
config: PI0Config,
**kwargs,
):
"""
"""Build the underlying PI0 model from `config`.
Args:
config: Policy configuration class instance.
config (`PI0Config`): Policy configuration class instance.
"""
require_package("transformers", extra="pi")
super().__init__(config)
@@ -794,7 +800,11 @@ class PI0Policy(PreTrainedPolicy):
strict: bool = True,
**kwargs,
) -> T:
"""Override the from_pretrained method to handle key remapping and display important disclaimer."""
"""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`.
"""
print(
"The PI0 model is a direct port of the OpenPI implementation. \n"
"This implementation follows the original OpenPI structure for compatibility. \n"
@@ -955,10 +965,13 @@ 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):
"""Reset internal state - called when environment resets."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
`select_action`.
"""
self._action_queue = deque(maxlen=self.config.n_action_steps)
self._queues = {
ACTION: deque(maxlen=self.config.n_action_steps),
@@ -1046,18 +1059,20 @@ class PI0Policy(PreTrainedPolicy):
return images, img_masks
def prepare_state(self, batch):
"""Pad state"""
"""Zero-pad the observation state to `config.max_state_dim`."""
state = pad_vector(batch[OBS_STATE], self.config.max_state_dim)
return state
def prepare_action(self, batch):
"""Pad action"""
"""Zero-pad the target action to `config.max_action_dim`."""
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
return actions
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select a single action given environment observations."""
"""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.
"""
assert not self._rtc_enabled(), (
"RTC is not supported for select_action, use it with predict_action_chunk"
)
@@ -1074,7 +1089,9 @@ class PI0Policy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
"""Predict a chunk of actions given environment observations."""
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Runs the flow-matching
sampler (`config.num_inference_steps` denoising steps) to generate the chunk.
"""
self.eval()
# Prepare inputs
@@ -1092,13 +1109,19 @@ class PI0Policy(PreTrainedPolicy):
return actions
def forward(self, batch: dict[str, Tensor], reduction: str = "mean") -> tuple[Tensor, dict]:
"""Run the batch through the model and compute the loss for training.
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the flow-matching loss between
the model's predicted and target velocity fields.
Args:
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
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.
"""
# Prepare inputs
images, img_masks = self._preprocess_images(batch)
+6 -13
View File
@@ -37,8 +37,7 @@ 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
@@ -46,8 +45,7 @@ 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
@@ -80,8 +78,7 @@ 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.
@@ -99,8 +96,7 @@ 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.
@@ -115,15 +111,12 @@ def make_pi0_pre_post_processors(
2. Unnormalizing the output features to their original scale.
Args:
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.
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.
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,6 +28,129 @@ 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"
@@ -104,6 +227,7 @@ 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
@@ -146,6 +270,7 @@ 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,
@@ -155,6 +280,7 @@ 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,
@@ -164,12 +290,15 @@ 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
+36 -13
View File
@@ -709,12 +709,17 @@ class PI05Pytorch(nn.Module): # see openpi `PI0Pytorch`
class PI05Policy(PreTrainedPolicy):
"""PI05 Policy for LeRobot."""
"""PyTorch port of Physical Intelligence's PI0.5 vision-language-action policy, generating action
chunks via flow matching.
"""
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__(
@@ -722,9 +727,10 @@ class PI05Policy(PreTrainedPolicy):
config: PI05Config,
**kwargs,
):
"""
"""Build the underlying PI0.5 model from `config`.
Args:
config: Policy configuration class instance.
config (`PI05Config`): Policy configuration class instance.
"""
require_package("transformers", extra="pi")
super().__init__(config)
@@ -759,7 +765,11 @@ class PI05Policy(PreTrainedPolicy):
strict: bool = True,
**kwargs,
) -> T:
"""Override the from_pretrained method to handle key remapping and display important disclaimer."""
"""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`.
"""
print(
"The PI05 model is a direct port of the OpenPI implementation. \n"
"This implementation follows the original OpenPI structure for compatibility. \n"
@@ -924,10 +934,13 @@ 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):
"""Reset internal state - called when environment resets."""
"""See [`~policies.pretrained.PreTrainedPolicy.reset`]. Clears the cached action queue used by
`select_action`.
"""
self._action_queue = deque(maxlen=self.config.n_action_steps)
self._queues = {
ACTION: deque(maxlen=self.config.n_action_steps),
@@ -1016,13 +1029,15 @@ class PI05Policy(PreTrainedPolicy):
return images, img_masks
def prepare_action(self, batch):
"""Pad action"""
"""Zero-pad the target action to `config.max_action_dim`."""
actions = pad_vector(batch[ACTION], self.config.max_action_dim)
return actions
@torch.no_grad()
def select_action(self, batch: dict[str, Tensor]) -> Tensor:
"""Select a single action given environment observations."""
"""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.
"""
assert not self._rtc_enabled(), (
"RTC is not supported for select_action, use it with predict_action_chunk"
)
@@ -1039,7 +1054,9 @@ class PI05Policy(PreTrainedPolicy):
@torch.no_grad()
def predict_action_chunk(self, batch: dict[str, Tensor], **kwargs: Unpack[ActionSelectKwargs]) -> Tensor:
"""Predict a chunk of actions given environment observations."""
"""See [`~policies.pretrained.PreTrainedPolicy.predict_action_chunk`]. Runs the flow-matching
sampler (`config.num_inference_steps` denoising steps) to generate the chunk.
"""
self.eval()
# Prepare inputs
@@ -1056,13 +1073,19 @@ class PI05Policy(PreTrainedPolicy):
return actions
def forward(self, batch: dict[str, Tensor], reduction: str = "mean") -> tuple[Tensor, dict]:
"""Run the batch through the model and compute the loss for training.
"""See [`~policies.pretrained.PreTrainedPolicy.forward`]. Computes the flow-matching loss between
the model's predicted and target velocity fields.
Args:
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
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.
"""
# Prepare inputs
images, img_masks = self._preprocess_images(batch)
+20 -24
View File
@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
@@ -21,10 +22,9 @@ import numpy as np
import torch
from lerobot.configs import PipelineFeatureType, PolicyFeature
from lerobot.lerobot_types import TransitionKey
from lerobot.lerobot_types import EnvTransition, TransitionKey
from lerobot.processor import (
AbsoluteActionsProcessorStep,
ComplementaryDataProcessorStep,
PolicyAction,
PolicyProcessorPipeline,
ProcessorStep,
@@ -41,22 +41,25 @@ from .configuration_pi05 import PI05Config
@ProcessorStepRegistry.register(name="pi05_prepare_state_tokenizer_processor_step")
@dataclass
class Pi05PrepareStateTokenizerProcessorStep(ComplementaryDataProcessorStep):
"""
Processor step to prepare the state and tokenize the language input.
"""
class Pi05PrepareStateTokenizerProcessorStep(ProcessorStep):
"""Processor step to prepare the state and tokenize the language input."""
max_state_dim: int = 32
task_key: str = "task"
def complementary_data(self, complementary_data: dict[str, Any]) -> dict[str, Any]:
state = (self.transition.get(TransitionKey.OBSERVATION) or {}).get(OBS_STATE)
def __call__(self, transition: EnvTransition) -> EnvTransition:
transition = transition.copy()
state = transition.get(TransitionKey.OBSERVATION, {}).get(OBS_STATE)
if state is None:
raise ValueError("State is required for PI05")
tasks = complementary_data.get(self.task_key)
tasks = transition.get(TransitionKey.COMPLEMENTARY_DATA, {}).get(self.task_key)
if tasks is None:
raise ValueError("No task found in complementary data")
# TODO: check if this necessary
state = deepcopy(state)
# State should already be normalized to [-1, 1] by the NormalizerProcessorStep that runs before this step
# Discretize into 256 bins (see openpi `PaligemmaTokenizer.tokenize()`)
state_np = state.cpu().numpy()
@@ -69,18 +72,15 @@ class Pi05PrepareStateTokenizerProcessorStep(ComplementaryDataProcessorStep):
full_prompt = f"Task: {cleaned_text}, State: {state_str};\nAction: "
full_prompts.append(full_prompt)
complementary_data[self.task_key] = full_prompts
return complementary_data
def get_config(self) -> dict[str, Any]:
return {"task_key": self.task_key, "max_state_dim": self.max_state_dim}
transition[TransitionKey.COMPLEMENTARY_DATA][self.task_key] = full_prompts
# Normalize state to [-1, 1] range if needed (assuming it's already normalized by normalizer processor step!!)
# Discretize into 256 bins (see openpi `PaligemmaTokenizer.tokenize()`)
return transition
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,8 +91,7 @@ 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.
@@ -107,15 +106,12 @@ def make_pi05_pre_post_processors(
2. Unnormalizing the output features to their original scale.
Args:
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.
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.
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,6 +28,126 @@ 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"
@@ -100,6 +220,7 @@ 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
@@ -139,6 +260,7 @@ 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,
@@ -148,6 +270,7 @@ 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,
@@ -157,12 +280,15 @@ 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

Some files were not shown because too many files have changed in this diff Show More