Compare commits

..

26 Commits

Author SHA1 Message Date
Pepijn
63dedac255 fix(ci): downgrade contents permission to read in claude.yml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 19:19:31 +02:00
Pepijn
b0286b10cf chore: remove root CLAUDE.md (moved to .github/CLAUDE.md)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 18:04:48 +02:00
Pepijn
7a8b02cd32 refactor(ci): move CLAUDE.md to .github/ to keep repo root clean
CLAUDE.md is CI-only config — moving it to .github/ ensures it is not
visible at the repo root when contributors clone lerobot. Both workflows
now explicitly reference .github/CLAUDE.md in their prompt/system-prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 18:03:06 +02:00
Pepijn
892e9f13b7 docs(claude): remove LOC minimization guideline from CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:59:55 +02:00
Pepijn
4b8436aefa feat(ci): restrict @claude trigger to repo owners, members, and collaborators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:57:50 +02:00
Pepijn
9d97426cb8 Merge branch 'main' into fix/claude-code-action-precommit 2026-04-08 17:56:32 +02:00
Pepijn
e8f504edaa feat(ci): use claude-opus-4-6 for PR reviews
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:56:01 +02:00
Pepijn
db7334a384 docs(claude): add Processor to core abstractions in CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:54:17 +02:00
Pepijn
5de7aa5a4f refactor(envs): move benchmark dispatch into EnvConfig subclasses (#3272)
* docs(benchmarks): add benchmark integration guide and standardize benchmark docs

Add a comprehensive guide for adding new benchmarks to LeRobot, and
refactor the existing LIBERO and Meta-World docs to follow the new
standardized template.

* refactor(envs): move dispatch logic from factory into EnvConfig subclasses

Replace hardcoded if/elif chains in factory.py with create_envs() and
get_env_processors() methods on EnvConfig. New benchmarks now only need
to register a config subclass — no factory.py edits required.

Net -23 lines: factory.py shrinks from ~200 to ~70 lines of logic.

* docs(benchmarks): clean up adding-benchmarks guide for clarity

Rewrite for simpler language, better structure, and easier navigation.
Move quick-reference table to the top, fold eval explanation into
architecture section, condense the doc template to a bulleted outline.

* fix link

* fix task count

* fix(tests): fix 3 failing dispatch tests

- test_registry_all_types: skip non-EnvConfig stubs (e.g. TestPluginConfig)
- test_processors_delegation: use None instead of abstract PreTrainedConfig
- test_custom_get_env_processors_override: use DataProcessorPipeline for isinstance check (PolicyProcessorPipeline is a subscripted generic)

* fix: enable SmolVLA eval on LIBERO with custom camera mappings

- Thread camera_name_mapping from LiberoEnv config through to gym envs
- Sync features_map with camera_name_mapping in LiberoEnv.__post_init__
- Fix render() to use first available camera instead of hardcoded "image"
- Handle non-dict final_info in rollout by falling back to info["is_success"]
- Add use_peft legacy field to SmolVLAConfig for checkpoint compat
- Add defaults to GR00TN15Config init=False fields for transformers 5.3

Made-with: Cursor

* fix: use direct AutoresetMode import for gymnasium compat

Made-with: Cursor

* fix: handle gymnasium < 1.0 without AutoresetMode

Made-with: Cursor

* refactor: revert policy changes, keep env-only camera mapping fixes

- Revert GR00T N1.5 default_factory/default changes (transformers compat)
- Revert SmolVLA use_peft legacy field
- Apply ruff formatting fixes
- camera_name_mapping stays entirely in env/eval layer (no policy changes)

Made-with: Cursor

* Update docs/source/env_processor.mdx

Co-authored-by: Khalil Meftah <khalil.meftah@huggingface.co>
Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com>

* Update docs/source/env_processor.mdx

Co-authored-by: Khalil Meftah <khalil.meftah@huggingface.co>
Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com>

* Update docs/source/env_processor.mdx

Co-authored-by: Khalil Meftah <khalil.meftah@huggingface.co>
Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com>

* fix(eval): raise RuntimeError for unsupported final_info format (Gymnasium < 1.0)

Made-with: Cursor

* style: fix markdown code fences in env_processor.mdx

Made-with: Cursor

* docs: remove duplicate code blocks in env_processor.mdx

Made-with: Cursor

* style: revert quadruple backticks to triple (prettier compat)

* docs(env_processor): add EnvConfig subclass step and policy_cfg examples

- Add missing '### 2. Update Your EnvConfig Subclass' section with
  get_env_processors() snippet
- Update factory usage example to show policy_cfg parameter and
  keyword-argument style for both SmolVLA and ACT cases

* docs(env_processor): rename step 2 and fix policy_cfg examples

- Rename '### 2. Update the Factory' → '### 2. Update Your EnvConfig Subclass'
- Update factory usage examples to use keyword-argument style with
  policy_cfg parameter for both SmolVLA and ACT cases

---------

Signed-off-by: Pepijn <138571049+pkooij@users.noreply.github.com>
Co-authored-by: Khalil Meftah <khalil.meftah@huggingface.co>
2026-04-08 17:48:58 +02:00
Pepijn
fc8d89b128 feat(ci): add CLAUDE.md and improve claude-code-action workflows
- Add CLAUDE.md with lerobot-specific review instructions (core abstractions,
  engineering principles, ML-specific checks, PR checklist)
- Enable use_sticky_comment: true on both workflows (single updating comment per PR)
- Add structured lerobot-specific review prompt to claude-code-review.yml
- Upgrade permissions: contents/pull-requests/issues write for interactive claude.yml
- Add actions: read to claude-code-review.yml for CI log access
- Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true to suppress Node.js 20 deprecation warnings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:47:41 +02:00
Pepijn
e0bde22193 fix(ci): pin claude-code-action to commit SHA and add persist-credentials: false
Fixes pre-commit zizmor failures from PR #3322:
- Pin anthropics/claude-code-action@v1 to commit hash (26ddc358) to satisfy blanket pinning policy
- Add persist-credentials: false to actions/checkout steps to suppress credential-persistence warning
- Remove trailing blank lines to satisfy end-of-file-fixer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:34:53 +02:00
Pauline Bailly-Masson
055f20f658 "Claude Code Review workflow" 2026-04-08 17:22:05 +02:00
Pauline Bailly-Masson
30d2fe3bb3 "Claude PR Assistant workflow" 2026-04-08 17:22:03 +02:00
Steven Palma
4eecbad32b chore(dependencies): Bump lerobot to 0.5.2 (#3307)
* chore(dependencies): Bump lerobot to 0.5.2

* chore(dependecies): upgrade uv.lock
2026-04-07 17:17:33 +02:00
Pauline Bailly-Masson
1396b9fab7 🔒 Pin GitHub Actions to commit SHAs (#3265)
* 🔒 pin quality.yml actions to commit SHAs

* 🔒 pin fast_tests.yml actions to commit SHAs

* 🔒 pin full_tests.yml actions to commit SHAs

* 🔒 pin documentation.yml actions to commit SHAs

* 🔒 pin documentation-upload-pr.yml actions to commit SHAs

* 🔒 pin release.yml actions to commit SHAs

* 🔒 pin security.yml actions to commit SHAs

---------

Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
2026-04-07 16:11:14 +02:00
Francesco Capuano
7c032f19fc feat(dataset): registering torchvision transforms (#3153)
* add: a flexible transformation registry

* fix: image transforms can be set both at init and after

* add: tests

* fix: take in review

* feat(datasets): add image transform setters

* fix: pre-commit

* fix: CI

---------

Signed-off-by: Francesco Capuano <74058581+fracapuano@users.noreply.github.com>
2026-04-07 15:59:11 +02:00
Anthony Chan
e2f27bf71b Fix lerobot_train script without interpolation (#3281) 2026-04-07 15:50:18 +02:00
Steven Palma
ea36a4a176 chore(docs): new badge for readme (#3303) 2026-04-07 10:47:03 +02:00
Steven Palma
399b3c9ba5 chore(dependencies): update uv.lock (#3302)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-07 09:49:00 +02:00
Steven Palma
913041e753 fix(ci): latest deps tests permissions (#3296)
* fix(ci): latest deps tests permissions

* fix(ci): force push dep update branch

* fix(ci): change secret for permissions & Ci trigger
2026-04-06 14:56:05 +02:00
Steven Palma
2b541ddd4c docs(ci): add readme for dockerfile (#3295) 2026-04-06 13:22:45 +02:00
Steven Palma
50a1e67e94 feat(ci): add uv.lock (#3292)
* feat(ci): add uv.lock

* feat(ci): use uv.lock in CI PR testing

* chore(ci): rename nightly to docker publish and test

* feat(ci): automated update of uv.lock + remove unbound check + docker images now use uv.lock

* fix(ci): add --force-with-lease + set -e for silent erros
2026-04-06 12:23:37 +02:00
Steven Palma
d60a700d2b chore(policy): multi dit docs (#3285)
* docs(policy): add libero results multi task dit + remove readme in src code

* docs(policy): add hyperlink to doc file in src code

* chore(style): pre-commit
2026-04-05 21:23:13 +02:00
Steven Palma
8c3d4cf900 chore(docs): no policy readme in src code (#3286)
* chore(docs): move policies readme out of src code

* chore(docs): create symlink for policy readme
2026-04-05 19:25:38 +02:00
Caroline Pascal
b6e60a6e30 chore(dependencies): bump minimum torch from 2.2.1 to 2.7 (#3156)
* feat(ffmpeg): updating ffmpeg verion to 8.X

* Revert "feat(ffmpeg): updating ffmpeg verion to 8.X"

This reverts commit bb0f03185c.

* chore(pyproject): updating pyproject to fit the minimally required version of torchcodec

* chore(docs): updating doc with specific instructions for ffmpeg/torchcodec installation

* fix(typo): reverting ceiling bound on pytorch to 2.11.0

* chore(format): removing empty line

* chore(typo): fixing typo

* chore(docs): adding warning in case of torchcodec/ffmpeg version mismatch

* chore(docs): applying comments

* chore(docs): adding uv commands for evdev on WSL

* fix(typo): fixing typo

* fix(typo): fixing typos again

* chore(ruff): format

* fix(evdev install): splitting evdev install instructions between conda and uv

* chore(ruff): format

---------

Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
2026-04-05 19:24:43 +02:00
Steven Palma
3596681d94 docs(policy): fix gr00t license docs (#3284) 2026-04-05 19:09:15 +02:00
43 changed files with 7100 additions and 564 deletions

86
.github/CLAUDE.md vendored Normal file
View File

@@ -0,0 +1,86 @@
# LeRobot — Claude Code Instructions
You are a senior robotics ML engineer reviewing code for **LeRobot**, a PyTorch framework for real-world robot learning.
Apply these principles to every PR review, fix, or task.
---
## Core Abstractions
These are the load-bearing types. Handle them with care — breaking changes here affect every user.
| Type | Location | Role |
| ---------------- | ---------------------------- | ------------------------------------------------------------ |
| `LeRobotDataset` | `src/lerobot/datasets/` | Streaming replay buffer; HF Hub integration |
| `Policy` | `src/lerobot/policies/` | Base class for all learning agents (ACT, Diffusion, SARM, …) |
| `Robot` | `src/lerobot/robots/` | Hardware abstraction; carries `_output_pipeline` |
| `Teleoperator` | `src/lerobot/teleoperators/` | Leader-side hardware abstraction; carries `_output_pipeline` |
| `Env` | `src/lerobot/envs/` | Gym-like robotics environments |
| `Processor` | `src/lerobot/processor/` | Data transformation pipelines attached to robots/teleops |
**Never break their public APIs without a migration note and explicit user approval.**
---
## Engineering Principles
### Code quality
- Explicit over magic — no hidden control flow, no implicit state.
- No deep inheritance trees. Prefer composition.
- No decorative comment separators (`===`, `---`, etc.).
- Add comments only where the logic is non-obvious.
- No over-engineering. YAGNI applies strictly.
### Type safety
- All new and modified Python code must be fully typed (PEP 484).
- `mypy --strict` must pass on changed files.
- Do not widen or weaken existing type signatures.
### Backwards compatibility
- Public API changes require migration notes.
- Additive changes are preferred over modifications.
- `so100_follower` / `so101_follower` are aliases — never bleed changes there unintentionally.
### HF ecosystem
- Use `push_to_hub()`, HF Hub dataset streaming, and `evaluate` scripts.
- Dataset changes must preserve streaming compatibility.
- Prefer reusing HF primitives over rolling custom solutions.
---
## PR Review Checklist
Before approving or marking P1 issues resolved, verify:
- [ ] `pre-commit run -a` would pass (ruff, mypy, typos, zizmor, bandit)
- [ ] All new/modified code is typed and passes `mypy --strict`
- [ ] New features have unit tests; no silent behavioral changes
- [ ] Public APIs of `LeRobotDataset`, `Policy`, `Robot`, `Teleoperator`, `Env` are unchanged (or migration note present)
- [ ] HF Hub streaming still works for dataset changes
- [ ] No unnecessary abstractions introduced
- [ ] No breaking changes to training scripts (`lerobot-train`, `lerobot-eval`, `lerobot-record`)
---
## ML-Specific Checks
Flag these as **P1** if found:
- **Data leakage**: train and val/test splits must be constructed before any normalization or augmentation that uses train statistics.
- **Loss function errors**: verify reduction mode (`mean` vs `sum`), correct masking, correct shape alignment.
- **Gradient flow**: new modules must have gradients flowing (check `requires_grad`, no detached tensors in the loss path by accident).
- **Distributed training**: operations on tensors must be DDP-safe; no in-place ops on parameters; batch norm needs `SyncBatchNorm` if used.
- **Memory leaks**: no accumulation of tensors outside the training loop; `optimizer.zero_grad()` called correctly.
---
## What to Skip
- Don't flag style nitpicks on unchanged surrounding code.
- Don't propose refactors outside the PR's scope.
- Don't add docstrings or comments to code the PR didn't touch.
- Don't suggest speculative future features (YAGNI).

View File

@@ -0,0 +1,49 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
jobs:
claude-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
persist-credentials: false
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@26ddc358fe3befff50c5ec2f80304c90c763f6f8 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_sticky_comment: true
prompt: |
Read `.github/CLAUDE.md` for lerobot-specific conventions, then review this PR.
Provide structured, actionable feedback.
Focus areas (in priority order):
1. **Correctness**: Logic errors, off-by-ones, wrong tensor shapes, incorrect loss functions
2. **Type safety**: All new/modified Python code must pass `mypy --strict`; check for missing annotations
3. **Backwards compatibility**: Does this break `LeRobotDataset`, `Policy`, `Robot`, `Teleoperator`, `Env`, or `Processor` public APIs?
4. **Tests**: New features must have tests; no silent behavioral changes
5. **Code style**: Explicit over magic, no unnecessary abstractions, no decorative comments
6. **HF integration**: Dataset streaming, `push_to_hub`, HF Hub compatibility preserved?
7. **pre-commit**: Would `pre-commit run -a` pass? (ruff, mypy, typos, zizmor)
Format findings as P1 (must fix) / P2 (should fix) / P3 (nice to have).
Skip P3 if the PR is already high quality.
claude_args: '--model claude-opus-4-6'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

58
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.author_association == 'COLLABORATOR')) ||
(github.event_name == 'issues' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
(github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR'))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
persist-credentials: false
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@26ddc358fe3befff50c5ec2f80304c90c763f6f8 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_sticky_comment: true
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
claude_args: '--system-prompt "Read .github/CLAUDE.md for lerobot-specific conventions before responding."'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

View File

@@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# This workflow handles nightly testing & docker images publishing. # This workflow handles Docker image publishing & testing.
name: Nightly name: Docker Publish & Test
permissions: permissions:
contents: read contents: read
@@ -39,8 +39,8 @@ concurrency:
jobs: jobs:
# This job builds a CPU image for testing & distribution # This job builds a CPU image for testing & distribution
build-docker-cpu-nightly: build-docker-cpu:
name: Build CPU Docker for Nightly name: Build CPU Docker
runs-on: runs-on:
group: aws-general-8-plus group: aws-general-8-plus
if: github.repository == 'huggingface/lerobot' if: github.repository == 'huggingface/lerobot'
@@ -74,8 +74,8 @@ jobs:
tags: ${{ env.DOCKER_IMAGE_NAME_CPU }} tags: ${{ env.DOCKER_IMAGE_NAME_CPU }}
# This job builds a GPU image for testing & distribution # This job builds a GPU image for testing & distribution
build-docker-gpu-nightly: build-docker-gpu:
name: Build GPU Docker for Nightly name: Build GPU Docker
runs-on: runs-on:
group: aws-general-8-plus group: aws-general-8-plus
if: github.repository == 'huggingface/lerobot' if: github.repository == 'huggingface/lerobot'
@@ -109,9 +109,9 @@ jobs:
tags: ${{ env.DOCKER_IMAGE_NAME_GPU }} tags: ${{ env.DOCKER_IMAGE_NAME_GPU }}
# This job runs the E2E tests + pytest with all extras in the CPU image # This job runs the E2E tests + pytest with all extras in the CPU image
nightly-cpu-tests: cpu-tests:
name: Nightly CPU Tests name: CPU Tests
needs: [build-docker-cpu-nightly] needs: [build-docker-cpu]
runs-on: runs-on:
group: aws-g6-4xlarge-plus group: aws-g6-4xlarge-plus
env: env:
@@ -121,7 +121,7 @@ jobs:
TRITON_CACHE_DIR: /home/user_lerobot/.cache/triton TRITON_CACHE_DIR: /home/user_lerobot/.cache/triton
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
container: container:
image: ${{ needs.build-docker-cpu-nightly.outputs.image_tag }} # zizmor: ignore[unpinned-images] image: ${{ needs.build-docker-cpu.outputs.image_tag }} # zizmor: ignore[unpinned-images]
options: --shm-size "16gb" options: --shm-size "16gb"
credentials: credentials:
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }} username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
@@ -142,9 +142,9 @@ jobs:
run: make test-end-to-end run: make test-end-to-end
# This job runs the E2E tests + pytest with all extras in the GPU image # This job runs the E2E tests + pytest with all extras in the GPU image
nightly-gpu-tests: gpu-tests:
name: Nightly GPU Tests name: GPU Tests
needs: [build-docker-gpu-nightly] needs: [build-docker-gpu]
runs-on: runs-on:
group: aws-g6-4xlarge-plus group: aws-g6-4xlarge-plus
env: env:
@@ -154,7 +154,7 @@ jobs:
TRITON_CACHE_DIR: /home/user_lerobot/.cache/triton TRITON_CACHE_DIR: /home/user_lerobot/.cache/triton
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
container: container:
image: ${{ needs.build-docker-gpu-nightly.outputs.image_tag }} # zizmor: ignore[unpinned-images] image: ${{ needs.build-docker-gpu.outputs.image_tag }} # zizmor: ignore[unpinned-images]
options: --gpus all --shm-size "16gb" options: --gpus all --shm-size "16gb"
credentials: credentials:
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }} username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
@@ -175,9 +175,9 @@ jobs:
run: make test-end-to-end run: make test-end-to-end
# This job runs multi-GPU training tests with 4 GPUs # This job runs multi-GPU training tests with 4 GPUs
nightly-multi-gpu-tests: multi-gpu-tests:
name: Nightly Multi-GPU Tests name: Multi-GPU Tests
needs: [build-docker-gpu-nightly] needs: [build-docker-gpu]
runs-on: runs-on:
group: aws-g4dn-12xlarge # Instance with 4 GPUs group: aws-g4dn-12xlarge # Instance with 4 GPUs
env: env:
@@ -188,7 +188,7 @@ jobs:
CUDA_VISIBLE_DEVICES: "0,1,2,3" CUDA_VISIBLE_DEVICES: "0,1,2,3"
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
container: container:
image: ${{ needs.build-docker-gpu-nightly.outputs.image_tag }} # zizmor: ignore[unpinned-images] image: ${{ needs.build-docker-gpu.outputs.image_tag }} # zizmor: ignore[unpinned-images]
options: --gpus all --shm-size "16gb" options: --gpus all --shm-size "16gb"
credentials: credentials:
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }} username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}

View File

@@ -33,7 +33,7 @@ jobs:
github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.conclusion == 'success' &&
github.repository == 'huggingface/lerobot' github.repository == 'huggingface/lerobot'
uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main
with: with:
package_name: lerobot package_name: lerobot
secrets: secrets:

View File

@@ -55,7 +55,7 @@ jobs:
github.repository == 'huggingface/lerobot' github.repository == 'huggingface/lerobot'
permissions: permissions:
contents: read contents: read
uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main
with: with:
commit_sha: ${{ github.sha }} commit_sha: ${{ github.sha }}
package: lerobot package: lerobot
@@ -78,7 +78,7 @@ jobs:
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@90b4ee2c10b81b5c1a6367c4e6fc9e2fb510a7e3 # main
with: with:
commit_sha: ${{ github.event.pull_request.head.sha }} commit_sha: ${{ github.event.pull_request.head.sha }}
pr_number: ${{ github.event.number }} pr_number: ${{ github.event.number }}

View File

@@ -27,6 +27,7 @@ on:
- "tests/**" - "tests/**"
- ".github/workflows/**" - ".github/workflows/**"
- "pyproject.toml" - "pyproject.toml"
- "uv.lock"
- "Makefile" - "Makefile"
push: push:
branches: branches:
@@ -36,6 +37,7 @@ on:
- "tests/**" - "tests/**"
- ".github/workflows/**" - ".github/workflows/**"
- "pyproject.toml" - "pyproject.toml"
- "uv.lock"
- "Makefile" - "Makefile"
permissions: permissions:
@@ -63,7 +65,7 @@ jobs:
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
lfs: true lfs: true
@@ -81,14 +83,14 @@ jobs:
libusb-1.0-0-dev speech-dispatcher libgeos-dev portaudio19-dev libusb-1.0-0-dev speech-dispatcher libgeos-dev portaudio19-dev
- name: Setup uv and Python - name: Setup uv and Python
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with: with:
enable-cache: true enable-cache: true
version: ${{ env.UV_VERSION }} version: ${{ env.UV_VERSION }}
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Install lerobot with test extras - name: Install lerobot with test extras
run: uv sync --extra "test" run: uv sync --locked --extra "test"
- name: Login to Hugging Face - name: Login to Hugging Face
if: env.HF_USER_TOKEN != '' if: env.HF_USER_TOKEN != ''

View File

@@ -29,6 +29,7 @@ on:
- "tests/**" - "tests/**"
- ".github/workflows/**" - ".github/workflows/**"
- "pyproject.toml" - "pyproject.toml"
- "uv.lock"
- "Makefile" - "Makefile"
permissions: permissions:
@@ -62,7 +63,7 @@ jobs:
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }} HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
lfs: true lfs: true
persist-credentials: false persist-credentials: false
@@ -79,14 +80,14 @@ jobs:
speech-dispatcher libgeos-dev portaudio19-dev speech-dispatcher libgeos-dev portaudio19-dev
- name: Setup uv and Python - name: Setup uv and Python
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with: with:
enable-cache: true enable-cache: true
version: ${{ env.UV_VERSION }} version: ${{ env.UV_VERSION }}
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Install lerobot with all extras - name: Install lerobot with all extras
run: uv sync --extra all # TODO(Steven): Make flash-attn optional run: uv sync --locked --extra all # TODO(Steven): Make flash-attn optional
- name: Login to Hugging Face - name: Login to Hugging Face
if: env.HF_USER_TOKEN != '' if: env.HF_USER_TOKEN != ''
@@ -136,21 +137,21 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install git-lfs sudo apt-get install git-lfs
git lfs install git lfs install
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
lfs: true lfs: true
persist-credentials: false persist-credentials: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses] uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
with: with:
cache-binary: false cache-binary: false
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses] uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }} username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }} password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses] uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: . context: .
file: ./docker/Dockerfile.internal file: ./docker/Dockerfile.internal

View File

@@ -12,38 +12,81 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# This workflow handles full testing with unboud dependencies versions. # This workflow tests the project against the latest upstream dependencies
name: Unbound Dependency Tests # (within pyproject.toml constraints) and opens a PR to update uv.lock
# if the tests pass and the lockfile has changed.
name: Latest Dependency Tests
on: on:
# Allows running this workflow manually from the Actions tab # Allows running this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
# Run on the 1st and 15th of every month at 09:00 UTC # Runs at 03:00 UTC
# schedule: schedule:
# - cron: '0 2 1,15 * *' - cron: "0 3 * * *"
permissions:
contents: read
# Sets up the environment variables # Sets up the environment variables
env: env:
UV_VERSION: "0.8.0" UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.12" PYTHON_VERSION: "3.12"
DOCKER_IMAGE_NAME: huggingface/lerobot-gpu:unbound DOCKER_IMAGE_NAME: huggingface/lerobot-gpu:latest-deps
# Ensures that only the latest action is built, canceling older runs. # Ensures that only the latest run is active, canceling older runs.
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# This job runs the E2E tests + pytest with all unbound extras # This job upgrades the lockfile and checks if dependencies have changed
full-tests: upgrade-lock:
name: Full Unbound Tests name: Upgrade Lockfile
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'huggingface/lerobot' if: github.repository == 'huggingface/lerobot'
permissions:
contents: read
outputs:
changed: ${{ steps.diff.outputs.changed }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup uv and Python
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses]
with:
version: ${{ env.UV_VERSION }}
python-version: ${{ env.PYTHON_VERSION }}
- name: Upgrade uv.lock
run: uv lock --upgrade
- name: Check for changes
id: diff
run: |
if git diff --quiet uv.lock; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "uv.lock is up to date — no dependency changes."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "uv.lock has changed — running tests."
fi
- name: Upload updated lockfile
if: steps.diff.outputs.changed == 'true'
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
with:
name: uv-lock
path: uv.lock
# This job runs the full test suite with the upgraded dependencies
cpu-tests:
name: CPU Tests (Latest Deps)
needs: [upgrade-lock]
if: needs.upgrade-lock.outputs.changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
env: env:
MUJOCO_GL: egl MUJOCO_GL: egl
HF_HOME: /mnt/cache/.cache/huggingface HF_HOME: /mnt/cache/.cache/huggingface
@@ -55,6 +98,11 @@ jobs:
lfs: true lfs: true
persist-credentials: false persist-credentials: false
- name: Download updated lockfile
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
with:
name: uv-lock
# NOTE(Steven): Mount to `/mnt` to avoid the limited storage on `/home`. Consider cleaning default SDKs or using self-hosted runners for more space. # NOTE(Steven): Mount to `/mnt` to avoid the limited storage on `/home`. Consider cleaning default SDKs or using self-hosted runners for more space.
# (As of 2024-06-10, the runner's `/home` has only 6.2 GB free—8% of its 72 GB total.) # (As of 2024-06-10, the runner's `/home` has only 6.2 GB free—8% of its 72 GB total.)
- name: Setup /mnt storage - name: Setup /mnt storage
@@ -73,34 +121,32 @@ jobs:
version: ${{ env.UV_VERSION }} version: ${{ env.UV_VERSION }}
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Unbound dependencies
run: |
sed -i 's/,[[:space:]]*<[0-9\.]*//g' pyproject.toml
echo "Dependencies unbound:" && cat pyproject.toml
- name: Install lerobot with all extras - name: Install lerobot with all extras
run: uv sync --extra all # TODO(Steven): Make flash-attn optional run: uv sync --locked --extra all # TODO(Steven): Make flash-attn optional
- name: Login to Hugging Face - name: Login to Hugging Face
if: env.HF_USER_TOKEN != '' if: env.HF_USER_TOKEN != ''
run: | run: |
uv run hf auth login --token "$HF_USER_TOKEN" --add-to-git-credential uv run hf auth login --token "$HF_USER_TOKEN" --add-to-git-credential
uv run hf auth whoami uv run hf auth whoami
- name: Run pytest (all extras) - name: Run pytest (all extras)
run: uv run pytest tests -vv run: uv run pytest tests -vv --maxfail=10
- name: Run end-to-end tests - name: Run end-to-end tests
run: uv run make test-end-to-end run: uv run make test-end-to-end
# This job builds a GPU enabled image for testing # This job builds a GPU-enabled Docker image with the upgraded dependencies
build-and-push-docker: build-and-push-docker:
name: Build and Push Docker name: Build and Push Docker
needs: [upgrade-lock]
if: needs.upgrade-lock.outputs.changed == 'true'
permissions:
contents: read
runs-on: runs-on:
group: aws-general-8-plus group: aws-general-8-plus
if: github.repository == 'huggingface/lerobot'
outputs: outputs:
image_tag: ${{ env.DOCKER_IMAGE_NAME }} image_tag: ${{ env.DOCKER_IMAGE_NAME }}
env:
GITHUB_REF: ${{ github.ref }}
steps: steps:
- name: Install Git LFS - name: Install Git LFS
run: | run: |
@@ -111,6 +157,12 @@ jobs:
with: with:
lfs: true lfs: true
persist-credentials: false persist-credentials: false
- name: Download updated lockfile
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
with:
name: uv-lock
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses] uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
with: with:
@@ -127,14 +179,13 @@ jobs:
file: ./docker/Dockerfile.internal file: ./docker/Dockerfile.internal
push: true push: true
tags: ${{ env.DOCKER_IMAGE_NAME }} tags: ${{ env.DOCKER_IMAGE_NAME }}
build-args: |
UNBOUND_DEPS=true
# This job runs pytest with all unbound extras in a GPU enabled host # This job runs pytest with all extras on a GPU-enabled host
# It runs everytime a test image is created
gpu-tests: gpu-tests:
name: GPU Unbound Tests name: GPU Tests (Latest Deps)
needs: [build-and-push-docker] needs: [build-and-push-docker]
permissions:
contents: read
runs-on: runs-on:
group: aws-g6-4xlarge-plus group: aws-g6-4xlarge-plus
env: env:
@@ -159,17 +210,69 @@ jobs:
run: | run: |
hf auth login --token "$HF_USER_TOKEN" --add-to-git-credential hf auth login --token "$HF_USER_TOKEN" --add-to-git-credential
hf auth whoami hf auth whoami
- name: Fix ptxas permissions
run: chmod +x /lerobot/.venv/lib/python3.12/site-packages/triton/backends/nvidia/bin/ptxas
- name: Run pytest on GPU - name: Run pytest on GPU
run: pytest tests -vv run: pytest tests -vv --maxfail=10
- name: Run end-to-end tests - name: Run end-to-end tests
run: make test-end-to-end run: make test-end-to-end
# This job deletes the test image recently created # This job creates or updates a PR with the upgraded lockfile
# It runs everytime after the gpu-tests have finished open-pr:
delete-unbound-image: name: Open PR
name: Delete Unbound Image needs: [cpu-tests, gpu-tests, upgrade-lock]
if: success() && needs.upgrade-lock.outputs.changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GH_TOKEN: ${{ secrets.UPDATE_LOCK_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Download updated lockfile
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
with:
name: uv-lock
- name: Create or update PR
run: |
set -euo pipefail
BRANCH="auto/update-uv-lock"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git checkout -B "$BRANCH"
git add uv.lock
git commit -m "chore(dependencies): update uv.lock"
git push --force origin "$BRANCH"
# Create PR only if one doesn't already exist for this branch
EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')
if [ -z "$EXISTING_PR" ]; then
gh pr create \
--title "chore(dependencies): update uv.lock" \
--body "Automated update of \`uv.lock\` after successful latest dependency tests (CPU + GPU).
This PR upgrades all dependencies to their latest versions within the ranges specified in \`pyproject.toml\`." \
--head "$BRANCH" \
--base main
else
echo "PR #$EXISTING_PR already exists, branch has been updated."
fi
# This job deletes the temporary Docker image after tests complete
cleanup-docker:
name: Cleanup Docker Image
needs: [gpu-tests, build-and-push-docker] needs: [gpu-tests, build-and-push-docker]
if: always() && needs.build-and-push-docker.result == 'success' if: always() && needs.build-and-push-docker.result == 'success'
permissions:
contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get Docker Hub Token and Delete Image - name: Get Docker Hub Token and Delete Image
@@ -180,8 +283,7 @@ jobs:
IMAGE_FULL: ${{ needs.build-and-push-docker.outputs.image_tag }} IMAGE_FULL: ${{ needs.build-and-push-docker.outputs.image_tag }}
run: | run: |
IMAGE_NAME=$(echo "$IMAGE_FULL" | cut -d':' -f1) IMAGE_NAME=$(echo "$IMAGE_FULL" | cut -d':' -f1)
IMAGE_TAG=$(echo "$IMAGE_FULL" | cut -d':' -f2) IMAGE_TAG=$(echo "$IMAGE_FULL" | cut -d':' -f2-)
echo "Attempting to delete image: $IMAGE_NAME:$IMAGE_TAG" echo "Attempting to delete image: $IMAGE_NAME:$IMAGE_TAG"
TOKEN=$(curl -s -H "Content-Type: application/json" \ TOKEN=$(curl -s -H "Content-Type: application/json" \

View File

@@ -43,16 +43,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version: '3.12' python-version: '3.12'
- name: Run pre-commit hooks - name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1 # zizmor: ignore[unpinned-uses] uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
with: with:
extra_args: --all-files --show-diff-on-failure --color=always extra_args: --all-files --show-diff-on-failure --color=always

View File

@@ -38,12 +38,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version: '3.12' python-version: '3.12'
@@ -104,7 +104,7 @@ jobs:
- name: Publish to TestPyPI for pre-releases - name: Publish to TestPyPI for pre-releases
# True for tags like 'v0.2.0-rc1' # True for tags like 'v0.2.0-rc1'
if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-') if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-')
uses: pypa/gh-action-pypi-publish@v1.13.0 # zizmor: ignore[unpinned-uses, use-trusted-publishing] uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
verbose: true verbose: true
@@ -112,7 +112,7 @@ jobs:
- name: Publish to PyPI - name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
uses: pypa/gh-action-pypi-publish@v1.13.0 # zizmor: ignore[unpinned-uses, use-trusted-publishing] uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with: with:
verbose: true verbose: true
print-hash: true print-hash: true
@@ -127,7 +127,7 @@ jobs:
env: env:
MUJOCO_GL: egl MUJOCO_GL: egl
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
lfs: true lfs: true
persist-credentials: false persist-credentials: false
@@ -137,7 +137,7 @@ jobs:
git curl libglib2.0-0 libegl1-mesa-dev ffmpeg libusb-1.0-0-dev \ git curl libglib2.0-0 libegl1-mesa-dev ffmpeg libusb-1.0-0-dev \
speech-dispatcher libgeos-dev portaudio19-dev speech-dispatcher libgeos-dev portaudio19-dev
- name: Setup uv and Python - name: Setup uv and Python
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses] uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with: with:
enable-cache: true # zizmor: ignore[cache-poisoning] enable-cache: true # zizmor: ignore[cache-poisoning]
version: ${{ env.UV_VERSION }} version: ${{ env.UV_VERSION }}

View File

@@ -43,12 +43,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 # zizmor: ignore[unpinned-uses] uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Secret Scanning - name: Secret Scanning
uses: trufflesecurity/trufflehog@v3.90.0 # zizmor: ignore[unpinned-uses] uses: trufflesecurity/trufflehog@eafb8c5f6a06175141c27f17bcc17941853d0047 # v3.90.0
with: with:
extra_args: --only-verified extra_args: --only-verified

1
.gitignore vendored
View File

@@ -25,7 +25,6 @@ node_modules/
# Lock files # Lock files
poetry.lock poetry.lock
uv.lock
Pipfile.lock Pipfile.lock
### Build & Distribution ### ### Build & Distribution ###

View File

@@ -4,7 +4,8 @@
<div align="center"> <div align="center">
[![Tests](https://github.com/huggingface/lerobot/actions/workflows/nightly.yml/badge.svg?branch=main)](https://github.com/huggingface/lerobot/actions/workflows/nightly.yml?query=branch%3Amain) [![Tests](https://github.com/huggingface/lerobot/actions/workflows/latest_deps_tests.yml/badge.svg?branch=main)](https://github.com/huggingface/lerobot/actions/workflows/latest_deps_tests.yml?query=branch%3Amain)
[![Tests](https://github.com/huggingface/lerobot/actions/workflows/docker_publish.yml/badge.svg?branch=main)](https://github.com/huggingface/lerobot/actions/workflows/docker_publish.yml?query=branch%3Amain)
[![Python versions](https://img.shields.io/pypi/pyversions/lerobot)](https://www.python.org/downloads/) [![Python versions](https://img.shields.io/pypi/pyversions/lerobot)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/huggingface/lerobot/blob/main/LICENSE) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/huggingface/lerobot/blob/main/LICENSE)
[![Status](https://img.shields.io/pypi/status/lerobot)](https://pypi.org/project/lerobot/) [![Status](https://img.shields.io/pypi/status/lerobot)](https://pypi.org/project/lerobot/)

View File

@@ -73,17 +73,10 @@ ENV HOME=/home/user_lerobot \
RUN uv venv --python python${PYTHON_VERSION} RUN uv venv --python python${PYTHON_VERSION}
# Install Python dependencies for caching # Install Python dependencies for caching
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml README.md MANIFEST.in ./ COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
COPY --chown=user_lerobot:user_lerobot src/ src/ COPY --chown=user_lerobot:user_lerobot src/ src/
ARG UNBOUND_DEPS=false RUN uv sync --locked --extra all --no-cache
RUN if [ "$UNBOUND_DEPS" = "true" ]; then \
sed -i 's/,[[:space:]]*<[0-9\.]*//g' pyproject.toml; \
echo "Dependencies unbound:" && cat pyproject.toml; \
fi
RUN uv pip install --no-cache ".[all]"
RUN chmod +x /lerobot/.venv/lib/python${PYTHON_VERSION}/site-packages/triton/backends/nvidia/bin/ptxas RUN chmod +x /lerobot/.venv/lib/python${PYTHON_VERSION}/site-packages/triton/backends/nvidia/bin/ptxas

View File

@@ -61,17 +61,10 @@ ENV HOME=/home/user_lerobot \
RUN uv venv RUN uv venv
# Install Python dependencies for caching # Install Python dependencies for caching
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml README.md MANIFEST.in ./ COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
COPY --chown=user_lerobot:user_lerobot src/ src/ COPY --chown=user_lerobot:user_lerobot src/ src/
ARG UNBOUND_DEPS=false RUN uv sync --locked --extra all --no-cache
RUN if [ "$UNBOUND_DEPS" = "true" ]; then \
sed -i 's/,[[:space:]]*<[0-9\.]*//g' pyproject.toml; \
echo "Dependencies unbound:" && cat pyproject.toml; \
fi
RUN uv pip install --no-cache ".[all]"
# Copy the rest of the application code # Copy the rest of the application code
# Make sure to have the git-LFS files for testing # Make sure to have the git-LFS files for testing

77
docker/README.md Normal file
View File

@@ -0,0 +1,77 @@
# Docker
This directory contains Dockerfiles for running LeRobot in containerized environments. Both images are **built nightly from `main`** and published to Docker Hub with the full environment pre-baked — no dependency setup required.
## Pre-built Images
```bash
# CPU-only image (based on Dockerfile.user)
docker pull huggingface/lerobot-cpu:latest
# GPU image with CUDA support (based on Dockerfile.internal)
docker pull huggingface/lerobot-gpu:latest
```
## Quick Start
The fastest way to start training is to pull the GPU image and run `lerobot-train` directly. This is the same environment used for all of our CI, so it is a well-tested, batteries-included setup.
```bash
docker run -it --rm --gpus all --shm-size 16gb huggingface/lerobot-gpu:latest
# inside the container:
lerobot-train --policy.type=act --dataset.repo_id=lerobot/aloha_sim_transfer_cube_human
```
## Dockerfiles
### `Dockerfile.user` (CPU)
A lightweight image based on `python:3.12-slim`. Includes all Python dependencies and system libraries but does not include CUDA — there is no GPU support. Useful for exploring the codebase, running scripts, or working with robots, but not practical for training.
### `Dockerfile.internal` (GPU)
A CUDA-enabled image based on `nvidia/cuda`. This is the image for training — mostly used for internal interactions with the GPU cluster.
## Usage
### Running a pre-built image
```bash
# CPU
docker run -it --rm huggingface/lerobot-cpu:latest
# GPU
docker run -it --rm --gpus all --shm-size 16gb huggingface/lerobot-gpu:latest
```
### Building locally
From the repo root:
```bash
# CPU
docker build -f docker/Dockerfile.user -t lerobot-user .
docker run -it --rm lerobot-user
# GPU
docker build -f docker/Dockerfile.internal -t lerobot-internal .
docker run -it --rm --gpus all --shm-size 16gb lerobot-internal
```
### Multi-GPU training
To select specific GPUs, set `CUDA_VISIBLE_DEVICES` when launching the container:
```bash
# Use 4 GPUs
docker run -it --rm --gpus all --shm-size 16gb \
-e CUDA_VISIBLE_DEVICES=0,1,2,3 \
huggingface/lerobot-gpu:latest
```
### USB device access (e.g. robots, cameras)
```bash
docker run -it --device=/dev/ -v /dev/:/dev/ --rm huggingface/lerobot-cpu:latest
```

View File

@@ -90,11 +90,17 @@ The same policy can work with different environment processors, and the same env
```python ```python
# Use SmolVLA policy with LIBERO environment # Use SmolVLA policy with LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(libero_cfg) # Use SmolVLA policy with LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
env_cfg=libero_cfg,
policy_cfg=smolvla_cfg,
)
smolvla_preprocessor, smolvla_postprocessor = make_pre_post_processors(smolvla_cfg) smolvla_preprocessor, smolvla_postprocessor = make_pre_post_processors(smolvla_cfg)
# Or use ACT policy with the same LIBERO environment # Or use ACT policy with the same LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(libero_cfg) libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
env_cfg=libero_cfg,
policy_cfg=act_cfg,
)
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg) act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)
``` ```
@@ -203,7 +209,10 @@ def eval_main(cfg: EvalPipelineConfig):
) )
# Create environment processors (NEW!) # Create environment processors (NEW!)
env_preprocessor, env_postprocessor = make_env_pre_post_processors(env_cfg=cfg.env) env_preprocessor, env_postprocessor = make_env_pre_post_processors(
env_cfg=cfg.env,
policy_cfg=cfg.policy,
)
# Run evaluation with both processor types # Run evaluation with both processor types
eval_policy_all( eval_policy_all(
@@ -307,21 +316,22 @@ class MyEnvProcessorStep(ObservationProcessorStep):
return processed return processed
``` ```
### 2. Update the Factory ### 2. Update Your `EnvConfig` Subclass
```python ```python
# In src/lerobot/envs/factory.py # In src/lerobot/envs/configs.py
@EnvConfig.register_subclass("myenv")
@dataclass
class MyEnvConfig(EnvConfig):
# ... task/features/gym kwargs ...
def make_env_pre_post_processors(env_cfg: EnvConfig): def get_env_processors(self):
if isinstance(env_cfg, LiberoEnv) or "libero" in env_cfg.type: from lerobot.processor.pipeline import PolicyProcessorPipeline
preprocessor = PolicyProcessorPipeline(steps=[LiberoProcessorStep()])
elif isinstance(env_cfg, MyEnvConfig) or "myenv" in env_cfg.type:
preprocessor = PolicyProcessorPipeline(steps=[MyEnvProcessorStep()])
else:
preprocessor = PolicyProcessorPipeline(steps=[])
postprocessor = PolicyProcessorPipeline(steps=[]) return (
return preprocessor, postprocessor PolicyProcessorPipeline(steps=[MyEnvProcessorStep()]),
PolicyProcessorPipeline(steps=[]),
)
``` ```
### 3. Use in Evaluation ### 3. Use in Evaluation

View File

@@ -131,4 +131,4 @@ lerobot-record \
## License ## License
This model follows the **Apache 2.0 License**, consistent with the original [GR00T repository](https://github.com/NVIDIA/Isaac-GR00T). This model follows NVIDIA's proprietary license, consistent with the original [GR00T repository](https://github.com/NVIDIA/Isaac-GR00T). Future versions (starting from N1.7) will follow **Apache 2.0 License**.

View File

@@ -1,6 +1,6 @@
# Installation # Installation
This guide uses `conda` (via miniforge) to manage environments (recommended). If you prefer another environment manager (e.g. `uv`, `venv`), ensure you have Python >=3.12 and `ffmpeg` installed with the `libsvtav1` encoder, then skip ahead to [Environment Setup](#step-2-environment-setup). This guide uses `conda` (via miniforge) to manage environments (recommended). If you prefer another environment manager (e.g. `uv`, `venv`), ensure you have Python >=3.12 and support PyTorch >= 2.10, then skip ahead to [Environment Setup](#step-2-environment-setup).
## Step 1 (`conda` only): Install [`miniforge`](https://conda-forge.org/download/) ## Step 1 (`conda` only): Install [`miniforge`](https://conda-forge.org/download/)
@@ -20,7 +20,7 @@ Create a virtual environment with Python 3.12:
conda create -y -n lerobot python=3.12 conda create -y -n lerobot python=3.12
``` ```
</hfoption> </hfoption>
<hfoption id="uv"> <hfoption id="uv (PyTorch >= 2.10 only)">
```bash ```bash
uv python install 3.12 uv python install 3.12
uv venv --python 3.12 uv venv --python 3.12
@@ -32,48 +32,87 @@ uv venv --python 3.12
Then activate your virtual environment, you have to do this each time you open a shell to use lerobot: Then activate your virtual environment, you have to do this each time you open a shell to use lerobot:
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<hfoptions id="activate_venv"> <hfoptions id="activate_venv">
<hfoption id="conda">```bash <hfoption id="conda">
```bash
conda activate lerobot conda activate lerobot
```</hfoption>
<hfoption id="uv">
```bash
# Linux/macOSsource
source .venv/bin/activate
# Windows PowerShell
source .venv\Scripts\Activate.ps1
``` ```
</hfoption>
</hfoptions>
<!-- prettier-ignore-end -->
When using `conda`, install `ffmpeg` in your environment:
```bash
conda install ffmpeg -c conda-forge
ffmpeg -version # ffmpeg 8.X is not yet supported !
```
> [!TIP]
> This usually installs `ffmpeg 7.X` for your platform compiled with the `libsvtav1` encoder. If `libsvtav1` is not supported (check supported encoders with `ffmpeg -encoders`), you can:
>
> - _[On any platform]_ Explicitly install `ffmpeg 7.X` using:
>
> ```bash
> conda install ffmpeg=7.1.1 -c conda-forge
> ```
>
> - _[On Linux only]_ If you want to bring your own ffmpeg: Install [ffmpeg build dependencies](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#GettheDependencies) and [compile ffmpeg from source with libsvtav1](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu#libsvtav1), and make sure you use the corresponding ffmpeg binary to your install with `which ffmpeg`.
> [!NOTE] > [!NOTE]
> When installing LeRobot inside WSL (Windows Subsystem for Linux), make sure to install `evdev` with the following command: > When installing LeRobot inside WSL (Windows Subsystem for Linux), make sure to also install `evdev`:
> >
> ```bash > ```bash
> conda install evdev -c conda-forge > conda install evdev -c conda-forge
> ``` > ```
</hfoption>
<hfoption id="uv (PyTorch >= 2.10 only)">
```bash
# Linux/macOS
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\activate
```
> [!NOTE]
> When installing LeRobot inside WSL (Windows Subsystem for Linux), make sure to also install `evdev`:
>
> ```bash
> sudo apt install libevdev-dev
> uv pip install evdev
> ```
</hfoption>
</hfoptions>
<!-- prettier-ignore-end -->
### Install `ffmpeg` (for video decoding)
LeRobot uses [TorchCodec](https://github.com/meta-pytorch/torchcodec) for video decoding by default, which requires `ffmpeg`.
> [!NOTE]
> **Platform support:** TorchCodec is **not available** on macOS Intel (x86_64), Linux ARM (aarch64, arm64, armv7l), or Windows with PyTorch < 2.8. On these platforms, LeRobot automatically falls back to `pyav` — so you do not need to install `ffmpeg` and can skip to Step 3.
If your platform supports TorchCodec, install `ffmpeg` using one of the methods below:
<!-- prettier-ignore-start -->
<hfoptions id="install_ffmpeg">
<hfoption id="conda (any PyTorch version)">
Install `ffmpeg` in your conda environment. This works with **all PyTorch versions** and is **required for PyTorch < 2.10**:
```bash
conda install ffmpeg -c conda-forge
```
> [!TIP]
> This usually installs `ffmpeg 8.X` with the `libsvtav1` encoder. If you run into issues (e.g. `libsvtav1` missing — check with `ffmpeg -encoders` — or a version mismatch with `torchcodec`), you can explicitly install `ffmpeg 7.1.1` using:
>
> ```bash
> conda install ffmpeg=7.1.1 -c conda-forge
> ```
</hfoption>
<hfoption id="uv (PyTorch >= 2.10 only)">
Starting with **PyTorch >= 2.10** (TorchCodec ≥ 0.10), TorchCodec can dynamically link to a system-wide `ffmpeg` installation. This is useful when using `uv` or other non-`conda` environment managers:
```bash
# Ubuntu/Debian
sudo apt install ffmpeg
# macOS (Apple Silicon)
brew install ffmpeg
```
> [!IMPORTANT] > [!IMPORTANT]
> If you are using `uv` you will have to install `ffmpeg` system-wide (outside of the virtual environment). You rely on `uv` and `torchcodec` ability to dynamically link to the system `ffmpeg`. > System-wide `ffmpeg` is **only supported with PyTorch >= 2.10** (TorchCodec ≥ 0.10). For older PyTorch versions, you **must** use `conda install ffmpeg -c conda-forge` instead.
</hfoption>
</hfoptions>
<!-- prettier-ignore-end -->
## Step 3: Install LeRobot 🤗 ## Step 3: Install LeRobot 🤗

View File

@@ -331,6 +331,54 @@ lerobot-train \
--wandb.project=multitask_dit --wandb.project=multitask_dit
``` ```
## Libero Results
```
python -m lerobot.scripts.lerobot_train \
--dataset.repo_id=HuggingFaceVLA/libero \
--policy.type=multi_task_dit \
--policy.push_to_hub=false \
--output_dir="./outputs/multitask_dit_libero" \
--job_name="multitask-dit-libero" \
--wandb.enable=true \
--wandb.project=multitask_dit_libero \
--dataset.image_transforms.enable=true \
--dataset.image_transforms.max_num_transforms=4 \
--dataset.image_transforms.tfs='{"brightness":{"type":"ColorJitter","kwargs":{"brightness":[0.75,1.25]}},"contrast":{"type":"ColorJitter","kwargs":{"contrast":[0.6,1.4]}},"saturation":{"type":"ColorJitter","kwargs":{"saturation":[0.8,1.2]}},"hue":{"type":"ColorJitter","kwargs":{"hue":[-0.05,0.05]}},"sharpness":{"type":"SharpnessJitter","kwargs":{"sharpness":[0.6,1.4]}},"rotation":{"type":"RandomRotation","kwargs":{"degrees":[-5,5]}},"translation":{"type":"RandomAffine","kwargs":{"degrees":0,"translate":[0.1,0.1]}}}' \
--dataset.video_backend=torchcodec \
--policy.use_amp=true \
--policy.horizon=48 \
--policy.n_obs_steps=2 \
--policy.use_rope=true \
--policy.use_positional_encoding=false \
--policy.hidden_dim=768 \
--policy.num_layers=8 \
--policy.num_heads=12 \
--policy.dropout=0.1 \
--policy.timestep_embed_dim=256 \
--policy.objective=diffusion \
--policy.optimizer_lr=3e-4 \
--policy.optimizer_weight_decay=0 \
--policy.scheduler_warmup_steps=0 \
--policy.vision_encoder_name=openai/clip-vit-base-patch16 \
--policy.image_resize_shape=[256,256] \
--policy.image_crop_is_random=true \
--policy.text_encoder_name=openai/clip-vit-base-patch16 \
--policy.vision_encoder_lr_multiplier=0.1 \
--policy.device=cuda \
--num_workers=8 \
--save_freq=4000 \
--log_freq=100 \
--steps=100000 \
--batch_size=320
```
Results:
| LIBERO Spatial | LIBERO Object | LIBERO Goal | LIBERO 10 | Average |
| -------------- | ------------- | ----------- | --------- | ------- |
| 87.0 | 98.2 | 93.8 | 83.2 | 90.6 |
## References ## References
For more details on the technical implementation and architecture, see: For more details on the technical implementation and architecture, see:

View File

@@ -0,0 +1,91 @@
# π₀.₅ (pi05)
This repository contains the Hugging Face port of **π₀.₅**, adapted from [OpenPI](https://github.com/Physical-Intelligence/openpi) by the Physical Intelligence.
It is designed as a **Vision-Language-Action model with open-world generalization**.
---
## Model Overview
| Feature | π₀ | π₀.₅ |
| -------------------- | ------------------------------------------------------ | ----------------------------------------- |
| Time Conditioning | Concatenates time with actions via `action_time_mlp_*` | Uses `time_mlp_*` for AdaRMS conditioning |
| AdaRMS | Not used | Used in action expert |
| Tokenizer Length | 48 tokens | 200 tokens |
| Discrete State Input | False (Uses `state_proj` layer) | True |
| Parameter Count | Higher (includes state embedding) | Lower (no state embedding) |
---
## Relative Actions
π₀.₅ supports training with **relative actions**, where the model learns relative offsets
from the current robot state instead of absolute joint positions. This mirrors the
relative-action transform in OpenPI (`DeltaActions`) and can improve performance.
### How it works
1. **During preprocessing**, absolute actions are converted to relative offsets:
`relative = action - state` (for selected joints).
2. The relative actions are normalized using statistics computed from the relative distribution.
3. **During postprocessing**, predicted relative actions are converted back to absolute:
`absolute = relative + state`.
Joints listed in `relative_exclude_joints` (e.g., gripper) are kept absolute.
### Configuration
| Parameter | Type | Default | Description |
| ------------------------- | ----------- | ------------- | ---------------------------------------------------------------- |
| `use_relative_actions` | `bool` | `False` | Enable relative-action training |
| `relative_exclude_joints` | `list[str]` | `["gripper"]` | Joint names to keep absolute (matched by substring) |
| `action_feature_names` | `list[str]` | `None` | Auto-populated from dataset metadata at runtime by `make_policy` |
### Training example
```bash
python -m lerobot.scripts.lerobot_train \
--policy.type=pi05 \
--dataset.repo_id=your_org/your_dataset \
--policy.use_relative_actions=true \
--policy.relative_exclude_joints='["gripper"]'
```
When `use_relative_actions=true`, the training script automatically:
- Computes relative action statistics from the dataset (sampled chunk-level relative actions)
- Replaces the standard action stats with relative stats for normalization
- Broadcasts these stats across all ranks in distributed training
---
## Citation
If you use this work, please cite both **OpenPI** and the π₀.₅ paper:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{intelligence2025pi05visionlanguageactionmodelopenworld,
title = {π₀.₅: a Vision-Language-Action Model with Open-World Generalization},
author = {Physical Intelligence and Kevin Black and Noah Brown and James Darpinian and Karan Dhabalia and Danny Driess and Adnan Esmail and Michael Equi and Chelsea Finn and Niccolo Fusai and Manuel Y. Galliker and Dibya Ghosh and Lachy Groom and Karol Hausman and Brian Ichter and Szymon Jakubczak and Tim Jones and Liyiming Ke and Devin LeBlanc and Sergey Levine and Adrian Li-Bell and Mohith Mothukuri and Suraj Nair and Karl Pertsch and Allen Z. Ren and Lucy Xiaoyang Shi and Laura Smith and Jost Tobias Springenberg and Kyle Stachowicz and James Tanner and Quan Vuong and Homer Walke and Anna Walling and Haohuan Wang and Lili Yu and Ury Zhilinsky},
year = {2025},
eprint = {2504.16054},
archivePrefix= {arXiv},
primaryClass = {cs.LG},
url = {https://arxiv.org/abs/2504.16054},
}
```
---
## License
This port follows the **Apache 2.0 License**, consistent with the original [OpenPI repository](https://github.com/Physical-Intelligence/openpi).

View File

@@ -0,0 +1,108 @@
# π₀ (pi0)
This repository contains the Hugging Face port of **π₀**, adapted from [OpenPI](https://github.com/Physical-Intelligence/openpi) by the Physical Intelligence.
It is designed as a **Vision-Language-Action model for general robot control**.
---
## Model Overview
| Feature | π₀ | π₀.₅ |
| -------------------- | ------------------------------------------------------ | ----------------------------------------- |
| Time Conditioning | Concatenates time with actions via `action_time_mlp_*` | Uses `time_mlp_*` for AdaRMS conditioning |
| AdaRMS | Not used | Used in action expert |
| Tokenizer Length | 48 tokens | 200 tokens |
| Discrete State Input | False (Uses `state_proj` layer) | True |
| Parameter Count | Higher (includes state embedding) | Lower (no state embedding) |
---
## Relative Actions
π₀ supports training with **relative actions**, where the model learns relative offsets
from the current robot state instead of absolute joint positions. This mirrors the
relative-action transform in OpenPI (`DeltaActions`) and can improve performance.
### How it works
1. **During preprocessing**, absolute actions are converted to relative offsets:
`relative = action - state` (for selected joints).
2. The relative actions are normalized using statistics computed from the relative distribution.
3. **During postprocessing**, predicted relative actions are converted back to absolute:
`absolute = relative + state`.
Joints listed in `relative_exclude_joints` (e.g., gripper) are kept absolute.
### Configuration
| Parameter | Type | Default | Description |
| ------------------------- | ----------- | ------------- | ---------------------------------------------------------------- |
| `use_relative_actions` | `bool` | `False` | Enable relative-action training |
| `relative_exclude_joints` | `list[str]` | `["gripper"]` | Joint names to keep absolute (matched by substring) |
| `action_feature_names` | `list[str]` | `None` | Auto-populated from dataset metadata at runtime by `make_policy` |
### Training example
```bash
python -m lerobot.scripts.lerobot_train \
--policy.type=pi0 \
--dataset.repo_id=your_org/your_dataset \
--policy.use_relative_actions=true \
--policy.relative_exclude_joints='["gripper"]'
```
When `use_relative_actions=true`, the training script automatically:
- Computes relative action statistics from the dataset (sampled chunk-level relative actions)
- Replaces the standard action stats with relative stats for normalization
- Broadcasts these stats across all ranks in distributed training
### Recomputing stats for an existing dataset
If you want to precompute relative action stats offline, use `recompute_stats` from
`lerobot.datasets.dataset_tools`:
```python
from lerobot.datasets.lerobot_dataset import LeRobotDataset
from lerobot.datasets.dataset_tools import recompute_stats
dataset = LeRobotDataset("your_org/your_dataset")
dataset = recompute_stats(
dataset,
relative_action=True,
relative_exclude_joints=["gripper"],
)
```
---
## Citation
If you use this work, please cite both **OpenPI** and the π₀ paper:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{black2024pi0visionlanguageactionflowmodel,
title = {π₀: A Vision-Language-Action Flow Model for General Robot Control},
author = {Kevin Black and Noah Brown and Danny Driess and Adnan Esmail and Michael Equi and Chelsea Finn and Niccolo Fusai and Lachy Groom and Karol Hausman and Brian Ichter and Szymon Jakubczak and Tim Jones and Liyiming Ke and Sergey Levine and Adrian Li-Bell and Mohith Mothukuri and Suraj Nair and Karl Pertsch and Lucy Xiaoyang Shi and James Tanner and Quan Vuong and Anna Walling and Haohuan Wang and Ury Zhilinsky},
year = {2024},
eprint = {2410.24164},
archivePrefix= {arXiv},
primaryClass = {cs.LG},
url = {https://arxiv.org/abs/2410.24164},
}
```
---
## License
This port follows the **Apache 2.0 License**, consistent with the original [OpenPI repository](https://github.com/Physical-Intelligence/openpi).

View File

@@ -0,0 +1,38 @@
# Real-Time Chunking (RTC)
This module contains the LeRobot implementation of **Real-Time Chunking (RTC)**, an inference-time technique for flow-matching based policies.
**Note**: RTC is not a policy itself, but rather an inference enhancement that works with flow-matching based policies including [π₀](../pi0/), [π₀.₅](../pi05/), and [SmolVLA](../smolvla/).
---
## Citation
If you use Real-Time Chunking in your work, please cite:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{black2025realtimeexecutionactionchunking,
title={Real-Time Execution of Action Chunking Flow Policies},
author={Kevin Black and Manuel Y. Galliker and Sergey Levine},
year={2025},
eprint={2506.07339},
archivePrefix={arXiv},
primaryClass={cs.RO},
url={https://arxiv.org/abs/2506.07339},
}
```
---
## License
This implementation follows the **Apache 2.0 License**, consistent with the LeRobot project.

View File

@@ -0,0 +1,14 @@
## Paper
https://arxiv.org/abs/2509.25358
## Citation
```bibtex
@article{chen2025sarm,
title={SARM: Stage-Aware Reward Modeling for Long Horizon Robot Manipulation},
author={Chen, Qianzhong and Yu, Justin and Schwager, Mac and Abbeel, Pieter and Shentu, Yide and Wu, Philipp},
journal={arXiv preprint arXiv:2509.25358},
year={2025}
}
```

View File

@@ -25,7 +25,7 @@ discord = "https://discord.gg/s3KuuzsPFb"
[project] [project]
name = "lerobot" name = "lerobot"
version = "0.5.1" version = "0.5.2"
description = "🤗 LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch" description = "🤗 LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch"
dynamic = ["readme"] dynamic = ["readme"]
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
@@ -71,9 +71,9 @@ dependencies = [
"cmake>=3.29.0.1,<4.2.0", "cmake>=3.29.0.1,<4.2.0",
"packaging>=24.2,<26.0", "packaging>=24.2,<26.0",
"torch>=2.2.1,<2.11.0", "torch>=2.7,<2.11.0",
"torchcodec>=0.2.1,<0.11.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", "torchcodec>=0.3.0,<0.11.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10).
"torchvision>=0.21.0,<0.26.0", "torchvision>=0.22.0,<0.26.0",
"einops>=0.8.0,<0.9.0", "einops>=0.8.0,<0.9.0",
"opencv-python-headless>=4.9.0,<4.14.0", "opencv-python-headless>=4.9.0,<4.14.0",

View File

@@ -67,8 +67,7 @@ class EvalConfig:
# `batch_size` specifies the number of environments to use in a gym.vector.VectorEnv. # `batch_size` specifies the number of environments to use in a gym.vector.VectorEnv.
batch_size: int = 50 batch_size: int = 50
# `use_async_envs` specifies whether to use asynchronous environments (multiprocessing). # `use_async_envs` specifies whether to use asynchronous environments (multiprocessing).
# Defaults to True; automatically downgraded to SyncVectorEnv when batch_size=1. use_async_envs: bool = False
use_async_envs: bool = True
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.batch_size > self.n_episodes: if self.batch_size > self.n_episodes:

View File

@@ -151,9 +151,11 @@ class LeRobotDataset(torch.utils.data.Dataset):
``$HF_LEROBOT_HOME/hub``. ``$HF_LEROBOT_HOME/hub``.
episodes (list[int] | None, optional): If specified, this will only load episodes specified by episodes (list[int] | None, optional): If specified, this will only load episodes specified by
their episode_index in this list. Defaults to None. their episode_index in this list. Defaults to None.
image_transforms (Callable | None, optional): You can pass standard v2 image transforms from image_transforms (Callable | None, optional):
torchvision.transforms.v2 here which will be applied to visual modalities (whether they come Transform applied to visual modalities inside `__getitem__` after image decoding / tensor
from videos or images). Defaults to None. conversion. This works for both image-backed and video-backed observations and can later be
updated with `set_image_transforms()` or cleared with `clear_image_transforms()`.
Defaults to None.
delta_timestamps (dict[list[float]] | None, optional): _description_. Defaults to None. delta_timestamps (dict[list[float]] | None, optional): _description_. Defaults to None.
tolerance_s (float, optional): Tolerance in seconds used to ensure data timestamps are actually in tolerance_s (float, optional): Tolerance in seconds used to ensure data timestamps are actually in
sync with the fps value. It is used at the init of the dataset to make sure that each sync with the fps value. It is used at the init of the dataset to make sure that each
@@ -192,7 +194,8 @@ class LeRobotDataset(torch.utils.data.Dataset):
super().__init__() super().__init__()
self.repo_id = repo_id self.repo_id = repo_id
self._requested_root = Path(root) if root else None self._requested_root = Path(root) if root else None
self.image_transforms = image_transforms self.reader = None
self.set_image_transforms(image_transforms)
self.delta_timestamps = delta_timestamps self.delta_timestamps = delta_timestamps
self.episodes = episodes self.episodes = episodes
self.tolerance_s = tolerance_s self.tolerance_s = tolerance_s
@@ -475,6 +478,18 @@ class LeRobotDataset(torch.utils.data.Dataset):
f"}})" f"}})"
) )
def set_image_transforms(self, image_transforms: Callable | None) -> None:
"""Replace the transform applied to visual observations."""
if image_transforms is not None and not callable(image_transforms):
raise TypeError("image_transforms must be callable or None.")
self.image_transforms = image_transforms
if self.reader is not None:
self.reader._image_transforms = image_transforms
def clear_image_transforms(self) -> None:
"""Remove the transform applied to visual observations."""
self.set_image_transforms(None)
# ── Hub methods (stay on facade) ────────────────────────────────── # ── Hub methods (stay on facade) ──────────────────────────────────
def push_to_hub( def push_to_hub(

View File

@@ -89,12 +89,24 @@ class MultiLeRobotDataset(torch.utils.data.Dataset):
) )
self.disabled_features.update(extra_keys) self.disabled_features.update(extra_keys)
self.image_transforms = image_transforms
self.delta_timestamps = delta_timestamps self.delta_timestamps = delta_timestamps
# TODO(rcadene, aliberts): We should not perform this aggregation for datasets # TODO(rcadene, aliberts): We should not perform this aggregation for datasets
# with multiple robots of different ranges. Instead we should have one normalization # with multiple robots of different ranges. Instead we should have one normalization
# per robot. # per robot.
self.stats = aggregate_stats([dataset.meta.stats for dataset in self._datasets]) self.stats = aggregate_stats([dataset.meta.stats for dataset in self._datasets])
self.set_image_transforms(image_transforms)
def set_image_transforms(self, image_transforms: Callable | None) -> None:
"""Replace the transform for this dataset and its children."""
if image_transforms is not None and not callable(image_transforms):
raise TypeError("image_transforms must be callable or None.")
self.image_transforms = image_transforms
for dataset in getattr(self, "_datasets", []):
dataset.set_image_transforms(self.image_transforms)
def clear_image_transforms(self) -> None:
"""Remove the transform from this dataset and its children."""
self.set_image_transforms(None)
@property @property
def repo_id_to_index(self): def repo_id_to_index(self):

View File

@@ -75,14 +75,13 @@ class EnvConfig(draccus.ChoiceRegistry, abc.ABC):
def create_envs( def create_envs(
self, self,
n_envs: int, n_envs: int,
use_async_envs: bool = True, use_async_envs: bool = False,
) -> dict[str, dict[int, gym.vector.VectorEnv]]: ) -> dict[str, dict[int, gym.vector.VectorEnv]]:
"""Create {suite: {task_id: VectorEnv}}. """Create {suite: {task_id: VectorEnv}}.
Default: single-task env via gym.make(). Multi-task benchmarks override. Default: single-task env via gym.make(). Multi-task benchmarks override.
AsyncVectorEnv is the default for n_envs > 1; auto-downgraded to Sync for n_envs=1.
""" """
env_cls = gym.vector.AsyncVectorEnv if (use_async_envs and n_envs > 1) else gym.vector.SyncVectorEnv env_cls = gym.vector.AsyncVectorEnv if use_async_envs else gym.vector.SyncVectorEnv
if self.gym_id not in gym_registry: if self.gym_id not in gym_registry:
print(f"gym id '{self.gym_id}' not found, attempting to import '{self.package_name}'...") print(f"gym id '{self.gym_id}' not found, attempting to import '{self.package_name}'...")
@@ -102,7 +101,12 @@ class EnvConfig(draccus.ChoiceRegistry, abc.ABC):
def _make_one(): def _make_one():
return gym.make(self.gym_id, disable_env_checker=self.disable_env_checker, **self.gym_kwargs) return gym.make(self.gym_id, disable_env_checker=self.disable_env_checker, **self.gym_kwargs)
vec = env_cls([_make_one for _ in range(n_envs)], autoreset_mode=gym.vector.AutoresetMode.SAME_STEP) try:
from gymnasium.vector import AutoresetMode
vec = env_cls([_make_one for _ in range(n_envs)], autoreset_mode=AutoresetMode.SAME_STEP)
except ImportError:
vec = env_cls([_make_one for _ in range(n_envs)])
return {self.type: {0: vec}} return {self.type: {0: vec}}
def get_env_processors(self): def get_env_processors(self):
@@ -382,6 +386,12 @@ class LiberoEnv(EnvConfig):
else: else:
raise ValueError(f"Unsupported obs_type: {self.obs_type}") raise ValueError(f"Unsupported obs_type: {self.obs_type}")
if self.camera_name_mapping is not None:
mapped_agentview = self.camera_name_mapping.get("agentview_image", "image")
mapped_eye_in_hand = self.camera_name_mapping.get("robot0_eye_in_hand_image", "image2")
self.features_map[LIBERO_KEY_PIXELS_AGENTVIEW] = f"{OBS_IMAGES}.{mapped_agentview}"
self.features_map[LIBERO_KEY_PIXELS_EYE_IN_HAND] = f"{OBS_IMAGES}.{mapped_eye_in_hand}"
@property @property
def gym_kwargs(self) -> dict: def gym_kwargs(self) -> dict:
kwargs: dict[str, Any] = {"obs_type": self.obs_type, "render_mode": self.render_mode} kwargs: dict[str, Any] = {"obs_type": self.obs_type, "render_mode": self.render_mode}
@@ -389,12 +399,12 @@ class LiberoEnv(EnvConfig):
kwargs["task_ids"] = self.task_ids kwargs["task_ids"] = self.task_ids
return kwargs return kwargs
def create_envs(self, n_envs: int, use_async_envs: bool = True): def create_envs(self, n_envs: int, use_async_envs: bool = False):
from lerobot.envs.libero import create_libero_envs from lerobot.envs.libero import create_libero_envs
if self.task is None: if self.task is None:
raise ValueError("LiberoEnv requires a task to be specified") raise ValueError("LiberoEnv requires a task to be specified")
env_cls = gym.vector.AsyncVectorEnv if (use_async_envs and n_envs > 1) else gym.vector.SyncVectorEnv env_cls = gym.vector.AsyncVectorEnv if use_async_envs else gym.vector.SyncVectorEnv
return create_libero_envs( return create_libero_envs(
task=self.task, task=self.task,
n_envs=n_envs, n_envs=n_envs,
@@ -404,6 +414,7 @@ class LiberoEnv(EnvConfig):
env_cls=env_cls, env_cls=env_cls,
control_mode=self.control_mode, control_mode=self.control_mode,
episode_length=self.episode_length, episode_length=self.episode_length,
camera_name_mapping=self.camera_name_mapping,
) )
def get_env_processors(self): def get_env_processors(self):
@@ -457,12 +468,12 @@ class MetaworldEnv(EnvConfig):
"render_mode": self.render_mode, "render_mode": self.render_mode,
} }
def create_envs(self, n_envs: int, use_async_envs: bool = True): def create_envs(self, n_envs: int, use_async_envs: bool = False):
from lerobot.envs.metaworld import create_metaworld_envs from lerobot.envs.metaworld import create_metaworld_envs
if self.task is None: if self.task is None:
raise ValueError("MetaWorld requires a task to be specified") raise ValueError("MetaWorld requires a task to be specified")
env_cls = gym.vector.AsyncVectorEnv if (use_async_envs and n_envs > 1) else gym.vector.SyncVectorEnv env_cls = gym.vector.AsyncVectorEnv if use_async_envs else gym.vector.SyncVectorEnv
return create_metaworld_envs( return create_metaworld_envs(
task=self.task, task=self.task,
n_envs=n_envs, n_envs=n_envs,

View File

@@ -58,7 +58,7 @@ def make_env_pre_post_processors(
def make_env( def make_env(
cfg: EnvConfig | str, cfg: EnvConfig | str,
n_envs: int = 1, n_envs: int = 1,
use_async_envs: bool = True, use_async_envs: bool = False,
hub_cache_dir: str | None = None, hub_cache_dir: str | None = None,
trust_remote_code: bool = False, trust_remote_code: bool = False,
) -> dict[str, dict[int, gym.vector.VectorEnv]]: ) -> dict[str, dict[int, gym.vector.VectorEnv]]:

View File

@@ -150,17 +150,7 @@ class LiberoEnv(gym.Env):
self.init_state_id = self.episode_index # tie each sub-env to a fixed init state self.init_state_id = self.episode_index # tie each sub-env to a fixed init state
# Extract task metadata without allocating GPU resources (safe before fork). self._env = self._make_envs_task(task_suite, self.task_id)
task = task_suite.get_task(task_id)
self.task = task.name
self.task_description = task.language
self._task_bddl_file = os.path.join(
get_libero_path("bddl_files"), task.problem_folder, task.bddl_file
)
self._env: OffScreenRenderEnv | None = (
None # deferred — created on first reset() inside the worker subprocess
)
default_steps = 500 default_steps = 500
self._max_episode_steps = ( self._max_episode_steps = (
TASK_SUITE_MAX_STEPS.get(task_suite_name, default_steps) TASK_SUITE_MAX_STEPS.get(task_suite_name, default_steps)
@@ -231,32 +221,29 @@ class LiberoEnv(gym.Env):
low=ACTION_LOW, high=ACTION_HIGH, shape=(ACTION_DIM,), dtype=np.float32 low=ACTION_LOW, high=ACTION_HIGH, shape=(ACTION_DIM,), dtype=np.float32
) )
def _ensure_env(self) -> None:
"""Create the underlying OffScreenRenderEnv on first use.
Called inside the worker subprocess after fork(), so each worker gets
its own clean EGL context rather than inheriting a stale one from the
parent process (which causes EGL_BAD_CONTEXT crashes with AsyncVectorEnv).
"""
if self._env is not None:
return
env = OffScreenRenderEnv(
bddl_file_name=self._task_bddl_file,
camera_heights=self.observation_height,
camera_widths=self.observation_width,
)
env.reset()
self._env = env
def render(self): def render(self):
self._ensure_env()
raw_obs = self._env.env._get_observations() raw_obs = self._env.env._get_observations()
image = self._format_raw_obs(raw_obs)["pixels"]["image"] pixels = self._format_raw_obs(raw_obs)["pixels"]
image = next(iter(pixels.values()))
image = image[::-1, ::-1] # flip both H and W for visualization image = image[::-1, ::-1] # flip both H and W for visualization
return image return image
def _make_envs_task(self, task_suite: Any, task_id: int = 0):
task = task_suite.get_task(task_id)
self.task = task.name
self.task_description = task.language
task_bddl_file = os.path.join(get_libero_path("bddl_files"), task.problem_folder, task.bddl_file)
env_args = {
"bddl_file_name": task_bddl_file,
"camera_heights": self.observation_height,
"camera_widths": self.observation_width,
}
env = OffScreenRenderEnv(**env_args)
env.reset()
return env
def _format_raw_obs(self, raw_obs: RobotObservation) -> RobotObservation: def _format_raw_obs(self, raw_obs: RobotObservation) -> RobotObservation:
assert self._env is not None, "_format_raw_obs called before _ensure_env()"
images = {} images = {}
for camera_name in self.camera_name: for camera_name in self.camera_name:
image = raw_obs[camera_name] image = raw_obs[camera_name]
@@ -308,7 +295,6 @@ class LiberoEnv(gym.Env):
) )
def reset(self, seed=None, **kwargs): def reset(self, seed=None, **kwargs):
self._ensure_env()
super().reset(seed=seed) super().reset(seed=seed)
self._env.seed(seed) self._env.seed(seed)
raw_obs = self._env.reset() raw_obs = self._env.reset()
@@ -335,8 +321,6 @@ class LiberoEnv(gym.Env):
return observation, info return observation, info
def step(self, action: np.ndarray) -> tuple[RobotObservation, float, bool, bool, dict[str, Any]]: def step(self, action: np.ndarray) -> tuple[RobotObservation, float, bool, bool, dict[str, Any]]:
self._ensure_env()
assert self._env is not None
if action.ndim != 1: if action.ndim != 1:
raise ValueError( raise ValueError(
f"Expected action to be 1-D (shape (action_dim,)), " f"Expected action to be 1-D (shape (action_dim,)), "
@@ -356,19 +340,12 @@ class LiberoEnv(gym.Env):
) )
observation = self._format_raw_obs(raw_obs) observation = self._format_raw_obs(raw_obs)
if terminated: if terminated:
info["final_info"] = {
"task": self.task,
"task_id": self.task_id,
"done": bool(done),
"is_success": bool(is_success),
}
self.reset() self.reset()
truncated = False truncated = False
return observation, reward, terminated, truncated, info return observation, reward, terminated, truncated, info
def close(self): def close(self):
if self._env is not None: self._env.close()
self._env.close()
def _make_env_fns( def _make_env_fns(
@@ -382,6 +359,7 @@ def _make_env_fns(
init_states: bool, init_states: bool,
gym_kwargs: Mapping[str, Any], gym_kwargs: Mapping[str, Any],
control_mode: str, control_mode: str,
camera_name_mapping: dict[str, str] | None = None,
) -> list[Callable[[], LiberoEnv]]: ) -> list[Callable[[], LiberoEnv]]:
"""Build n_envs factory callables for a single (suite, task_id).""" """Build n_envs factory callables for a single (suite, task_id)."""
@@ -397,6 +375,7 @@ def _make_env_fns(
episode_index=episode_index, episode_index=episode_index,
n_envs=n_envs, n_envs=n_envs,
control_mode=control_mode, control_mode=control_mode,
camera_name_mapping=camera_name_mapping,
**local_kwargs, **local_kwargs,
) )
@@ -418,6 +397,7 @@ def create_libero_envs(
env_cls: Callable[[Sequence[Callable[[], Any]]], Any] | None = None, env_cls: Callable[[Sequence[Callable[[], Any]]], Any] | None = None,
control_mode: str = "relative", control_mode: str = "relative",
episode_length: int | None = None, episode_length: int | None = None,
camera_name_mapping: dict[str, str] | None = None,
) -> dict[str, dict[int, Any]]: ) -> dict[str, dict[int, Any]]:
""" """
Create vectorized LIBERO environments with a consistent return shape. Create vectorized LIBERO environments with a consistent return shape.
@@ -467,6 +447,7 @@ def create_libero_envs(
init_states=init_states, init_states=init_states,
gym_kwargs=gym_kwargs, gym_kwargs=gym_kwargs,
control_mode=control_mode, control_mode=control_mode,
camera_name_mapping=camera_name_mapping,
) )
out[suite_name][tid] = env_cls(fns) out[suite_name][tid] = env_cls(fns)
print(f"Built vec env | suite={suite_name} | task_id={tid} | n_envs={n_envs}") print(f"Built vec env | suite={suite_name} | task_id={tid} | n_envs={n_envs}")

View File

@@ -97,9 +97,8 @@ class MetaworldEnv(gym.Env):
self.visualization_height = visualization_height self.visualization_height = visualization_height
self.camera_name = camera_name self.camera_name = camera_name
self._env_name = self.task # already stripped of "metaworld-" prefix above self._env = self._make_envs_task(self.task)
self._env = None # deferred — created on first reset() inside the worker subprocess self._max_episode_steps = self._env.max_path_length
self._max_episode_steps = 500 # MT1 environments always have max_path_length=500
self.task_description = TASK_DESCRIPTIONS[self.task] self.task_description = TASK_DESCRIPTIONS[self.task]
self.expert_policy = TASK_POLICY_MAPPING[self.task]() self.expert_policy = TASK_POLICY_MAPPING[self.task]()
@@ -137,24 +136,6 @@ class MetaworldEnv(gym.Env):
self.action_space = spaces.Box(low=-1, high=1, shape=(ACTION_DIM,), dtype=np.float32) self.action_space = spaces.Box(low=-1, high=1, shape=(ACTION_DIM,), dtype=np.float32)
def _ensure_env(self) -> None:
"""Create the underlying MetaWorld env on first use.
Called inside the worker subprocess after fork(), so each worker gets
its own clean rendering context rather than inheriting a stale one from
the parent process (which causes crashes with AsyncVectorEnv).
"""
if self._env is not None:
return
mt1 = metaworld.MT1(self._env_name, seed=42)
env = mt1.train_classes[self._env_name](render_mode="rgb_array", camera_name=self.camera_name)
env.set_task(mt1.train_tasks[0])
if self.camera_name == "corner2":
env.model.cam_pos[2] = [0.75, 0.075, 0.7]
env.reset()
env._freeze_rand_vec = False # otherwise no randomization
self._env = env
def render(self) -> np.ndarray: def render(self) -> np.ndarray:
""" """
Render the current environment frame. Render the current environment frame.
@@ -162,13 +143,26 @@ class MetaworldEnv(gym.Env):
Returns: Returns:
np.ndarray: The rendered RGB image from the environment. np.ndarray: The rendered RGB image from the environment.
""" """
self._ensure_env()
image = self._env.render() image = self._env.render()
if self.camera_name == "corner2": if self.camera_name == "corner2":
# Images from this camera are flipped — correct them # Images from this camera are flipped — correct them
image = np.flip(image, (0, 1)) image = np.flip(image, (0, 1))
return image return image
def _make_envs_task(self, env_name: str):
mt1 = metaworld.MT1(env_name, seed=42)
env = mt1.train_classes[env_name](render_mode="rgb_array", camera_name=self.camera_name)
env.set_task(mt1.train_tasks[0])
if self.camera_name == "corner2":
env.model.cam_pos[2] = [
0.75,
0.075,
0.7,
] # corner2 position, similar to https://arxiv.org/pdf/2206.14244
env.reset()
env._freeze_rand_vec = False # otherwise no randomization
return env
def _format_raw_obs(self, raw_obs: np.ndarray) -> RobotObservation: def _format_raw_obs(self, raw_obs: np.ndarray) -> RobotObservation:
image = None image = None
if self._env is not None: if self._env is not None:
@@ -215,7 +209,6 @@ class MetaworldEnv(gym.Env):
observation (RobotObservation): The initial formatted observation. observation (RobotObservation): The initial formatted observation.
info (Dict[str, Any]): Additional info about the reset state. info (Dict[str, Any]): Additional info about the reset state.
""" """
self._ensure_env()
super().reset(seed=seed) super().reset(seed=seed)
raw_obs, info = self._env.reset(seed=seed) raw_obs, info = self._env.reset(seed=seed)
@@ -239,7 +232,6 @@ class MetaworldEnv(gym.Env):
truncated (bool): Whether the episode was truncated due to a time limit. truncated (bool): Whether the episode was truncated due to a time limit.
info (Dict[str, Any]): Additional environment info. info (Dict[str, Any]): Additional environment info.
""" """
self._ensure_env()
if action.ndim != 1: if action.ndim != 1:
raise ValueError( raise ValueError(
f"Expected action to be 1-D (shape (action_dim,)), " f"Expected action to be 1-D (shape (action_dim,)), "
@@ -271,8 +263,7 @@ class MetaworldEnv(gym.Env):
return observation, reward, terminated, truncated, info return observation, reward, terminated, truncated, info
def close(self): def close(self):
if self._env is not None: self._env.close()
self._env.close()
# ---- Main API ---------------------------------------------------------------- # ---- Main API ----------------------------------------------------------------

View File

@@ -1,37 +0,0 @@
# Multitask DiT Policy
## Citation
If you use this work, please cite the following works:
```bibtex
@misc{jones2025multitaskditpolicy,
author = {Bryson Jones},
title = {Dissecting and Open-Sourcing Multitask Diffusion Transformer Policy},
year = {2025},
url = {https://brysonkjones.substack.com/p/dissecting-and-open-sourcing-multitask-diffusion-transformer-policy},
note = {Blog post}
}
```
```bibtex
@misc{trilbmteam2025carefulexaminationlargebehaviormodels,
author = {TRI LBM Team},
title = {A Careful Examination of Large Behavior Models for Multitask Dexterous Manipulation},
year = {2025},
eprint = {arXiv:2507.05331},
archivePrefix = {arXiv},
primaryClass = {cs.RO},
url = {https://arxiv.org/abs/2507.05331}
}
```
```bibtex
@misc{bostondynamics2025largebehaviormodelsatlas,
author = {Boston Dynamics and TRI Research Team},
title = {Large Behavior Models and Atlas Find New Footing},
year = {2025},
url = {https://bostondynamics.com/blog/large-behavior-models-atlas-find-new-footing/},
note = {Blog post}
}
```

View File

@@ -0,0 +1 @@
../../../../docs/source/policy_multi_task_dit_README.md

View File

@@ -1,108 +0,0 @@
# π₀ (pi0)
This repository contains the Hugging Face port of **π₀**, adapted from [OpenPI](https://github.com/Physical-Intelligence/openpi) by the Physical Intelligence.
It is designed as a **Vision-Language-Action model for general robot control**.
---
## Model Overview
| Feature | π₀ | π₀.₅ |
| -------------------- | ------------------------------------------------------ | ----------------------------------------- |
| Time Conditioning | Concatenates time with actions via `action_time_mlp_*` | Uses `time_mlp_*` for AdaRMS conditioning |
| AdaRMS | Not used | Used in action expert |
| Tokenizer Length | 48 tokens | 200 tokens |
| Discrete State Input | False (Uses `state_proj` layer) | True |
| Parameter Count | Higher (includes state embedding) | Lower (no state embedding) |
---
## Relative Actions
π₀ supports training with **relative actions**, where the model learns relative offsets
from the current robot state instead of absolute joint positions. This mirrors the
relative-action transform in OpenPI (`DeltaActions`) and can improve performance.
### How it works
1. **During preprocessing**, absolute actions are converted to relative offsets:
`relative = action - state` (for selected joints).
2. The relative actions are normalized using statistics computed from the relative distribution.
3. **During postprocessing**, predicted relative actions are converted back to absolute:
`absolute = relative + state`.
Joints listed in `relative_exclude_joints` (e.g., gripper) are kept absolute.
### Configuration
| Parameter | Type | Default | Description |
| ------------------------- | ----------- | ------------- | ---------------------------------------------------------------- |
| `use_relative_actions` | `bool` | `False` | Enable relative-action training |
| `relative_exclude_joints` | `list[str]` | `["gripper"]` | Joint names to keep absolute (matched by substring) |
| `action_feature_names` | `list[str]` | `None` | Auto-populated from dataset metadata at runtime by `make_policy` |
### Training example
```bash
python -m lerobot.scripts.lerobot_train \
--policy.type=pi0 \
--dataset.repo_id=your_org/your_dataset \
--policy.use_relative_actions=true \
--policy.relative_exclude_joints='["gripper"]'
```
When `use_relative_actions=true`, the training script automatically:
- Computes relative action statistics from the dataset (sampled chunk-level relative actions)
- Replaces the standard action stats with relative stats for normalization
- Broadcasts these stats across all ranks in distributed training
### Recomputing stats for an existing dataset
If you want to precompute relative action stats offline, use `recompute_stats` from
`lerobot.datasets.dataset_tools`:
```python
from lerobot.datasets.lerobot_dataset import LeRobotDataset
from lerobot.datasets.dataset_tools import recompute_stats
dataset = LeRobotDataset("your_org/your_dataset")
dataset = recompute_stats(
dataset,
relative_action=True,
relative_exclude_joints=["gripper"],
)
```
---
## Citation
If you use this work, please cite both **OpenPI** and the π₀ paper:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{black2024pi0visionlanguageactionflowmodel,
title = {π₀: A Vision-Language-Action Flow Model for General Robot Control},
author = {Kevin Black and Noah Brown and Danny Driess and Adnan Esmail and Michael Equi and Chelsea Finn and Niccolo Fusai and Lachy Groom and Karol Hausman and Brian Ichter and Szymon Jakubczak and Tim Jones and Liyiming Ke and Sergey Levine and Adrian Li-Bell and Mohith Mothukuri and Suraj Nair and Karl Pertsch and Lucy Xiaoyang Shi and James Tanner and Quan Vuong and Anna Walling and Haohuan Wang and Ury Zhilinsky},
year = {2024},
eprint = {2410.24164},
archivePrefix= {arXiv},
primaryClass = {cs.LG},
url = {https://arxiv.org/abs/2410.24164},
}
```
---
## License
This port follows the **Apache 2.0 License**, consistent with the original [OpenPI repository](https://github.com/Physical-Intelligence/openpi).

View File

@@ -0,0 +1 @@
../../../../docs/source/policy_pi0_README.md

View File

@@ -1,91 +0,0 @@
# π₀.₅ (pi05)
This repository contains the Hugging Face port of **π₀.₅**, adapted from [OpenPI](https://github.com/Physical-Intelligence/openpi) by the Physical Intelligence.
It is designed as a **Vision-Language-Action model with open-world generalization**.
---
## Model Overview
| Feature | π₀ | π₀.₅ |
| -------------------- | ------------------------------------------------------ | ----------------------------------------- |
| Time Conditioning | Concatenates time with actions via `action_time_mlp_*` | Uses `time_mlp_*` for AdaRMS conditioning |
| AdaRMS | Not used | Used in action expert |
| Tokenizer Length | 48 tokens | 200 tokens |
| Discrete State Input | False (Uses `state_proj` layer) | True |
| Parameter Count | Higher (includes state embedding) | Lower (no state embedding) |
---
## Relative Actions
π₀.₅ supports training with **relative actions**, where the model learns relative offsets
from the current robot state instead of absolute joint positions. This mirrors the
relative-action transform in OpenPI (`DeltaActions`) and can improve performance.
### How it works
1. **During preprocessing**, absolute actions are converted to relative offsets:
`relative = action - state` (for selected joints).
2. The relative actions are normalized using statistics computed from the relative distribution.
3. **During postprocessing**, predicted relative actions are converted back to absolute:
`absolute = relative + state`.
Joints listed in `relative_exclude_joints` (e.g., gripper) are kept absolute.
### Configuration
| Parameter | Type | Default | Description |
| ------------------------- | ----------- | ------------- | ---------------------------------------------------------------- |
| `use_relative_actions` | `bool` | `False` | Enable relative-action training |
| `relative_exclude_joints` | `list[str]` | `["gripper"]` | Joint names to keep absolute (matched by substring) |
| `action_feature_names` | `list[str]` | `None` | Auto-populated from dataset metadata at runtime by `make_policy` |
### Training example
```bash
python -m lerobot.scripts.lerobot_train \
--policy.type=pi05 \
--dataset.repo_id=your_org/your_dataset \
--policy.use_relative_actions=true \
--policy.relative_exclude_joints='["gripper"]'
```
When `use_relative_actions=true`, the training script automatically:
- Computes relative action statistics from the dataset (sampled chunk-level relative actions)
- Replaces the standard action stats with relative stats for normalization
- Broadcasts these stats across all ranks in distributed training
---
## Citation
If you use this work, please cite both **OpenPI** and the π₀.₅ paper:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{intelligence2025pi05visionlanguageactionmodelopenworld,
title = {π₀.₅: a Vision-Language-Action Model with Open-World Generalization},
author = {Physical Intelligence and Kevin Black and Noah Brown and James Darpinian and Karan Dhabalia and Danny Driess and Adnan Esmail and Michael Equi and Chelsea Finn and Niccolo Fusai and Manuel Y. Galliker and Dibya Ghosh and Lachy Groom and Karol Hausman and Brian Ichter and Szymon Jakubczak and Tim Jones and Liyiming Ke and Devin LeBlanc and Sergey Levine and Adrian Li-Bell and Mohith Mothukuri and Suraj Nair and Karl Pertsch and Allen Z. Ren and Lucy Xiaoyang Shi and Laura Smith and Jost Tobias Springenberg and Kyle Stachowicz and James Tanner and Quan Vuong and Homer Walke and Anna Walling and Haohuan Wang and Lili Yu and Ury Zhilinsky},
year = {2025},
eprint = {2504.16054},
archivePrefix= {arXiv},
primaryClass = {cs.LG},
url = {https://arxiv.org/abs/2504.16054},
}
```
---
## License
This port follows the **Apache 2.0 License**, consistent with the original [OpenPI repository](https://github.com/Physical-Intelligence/openpi).

View File

@@ -0,0 +1 @@
../../../../docs/source/policy_pi05_README.md

View File

@@ -1,38 +0,0 @@
# Real-Time Chunking (RTC)
This module contains the LeRobot implementation of **Real-Time Chunking (RTC)**, an inference-time technique for flow-matching based policies.
**Note**: RTC is not a policy itself, but rather an inference enhancement that works with flow-matching based policies including [π₀](../pi0/), [π₀.₅](../pi05/), and [SmolVLA](../smolvla/).
---
## Citation
If you use Real-Time Chunking in your work, please cite:
```bibtex
@misc{openpi2024,
author = {Physical Intelligence Lab},
title = {OpenPI: PyTorch Implementation of π0 and π0.5 Policies},
year = {2024},
publisher = {GitHub},
howpublished = {\url{https://github.com/Physical-Intelligence/openpi}},
license = {Apache-2.0}
}
@misc{black2025realtimeexecutionactionchunking,
title={Real-Time Execution of Action Chunking Flow Policies},
author={Kevin Black and Manuel Y. Galliker and Sergey Levine},
year={2025},
eprint={2506.07339},
archivePrefix={arXiv},
primaryClass={cs.RO},
url={https://arxiv.org/abs/2506.07339},
}
```
---
## License
This implementation follows the **Apache 2.0 License**, consistent with the LeRobot project.

View File

@@ -0,0 +1 @@
../../../../docs/source/policy_rtc_README.md

View File

@@ -1,14 +0,0 @@
## Paper
https://arxiv.org/abs/2509.25358
## Citation
```bibtex
@article{chen2025sarm,
title={SARM: Stage-Aware Reward Modeling for Long Horizon Robot Manipulation},
author={Chen, Qianzhong and Yu, Justin and Schwager, Mac and Abbeel, Pieter and Shentu, Yide and Wu, Philipp},
journal={arXiv preprint arXiv:2509.25358},
year={2025}
}
```

View File

@@ -0,0 +1 @@
../../../../docs/source/policy_sarm_README.md

View File

@@ -47,7 +47,6 @@ You can learn about the CLI options for this script in the `EvalPipelineConfig`
""" """
import concurrent.futures as cf import concurrent.futures as cf
import copy
import json import json
import logging import logging
import threading import threading
@@ -57,6 +56,7 @@ from collections.abc import Callable
from contextlib import nullcontext from contextlib import nullcontext
from copy import deepcopy from copy import deepcopy
from dataclasses import asdict from dataclasses import asdict
from functools import partial
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
from typing import Any, TypedDict from typing import Any, TypedDict
@@ -73,6 +73,7 @@ from lerobot.configs import parser
from lerobot.configs.eval import EvalPipelineConfig from lerobot.configs.eval import EvalPipelineConfig
from lerobot.envs.factory import make_env, make_env_pre_post_processors from lerobot.envs.factory import make_env, make_env_pre_post_processors
from lerobot.envs.utils import ( from lerobot.envs.utils import (
add_envs_task,
check_env_attributes_and_types, check_env_attributes_and_types,
close_envs, close_envs,
preprocess_observation, preprocess_observation,
@@ -165,9 +166,9 @@ def rollout(
if return_observations: if return_observations:
all_observations.append(deepcopy(observation)) all_observations.append(deepcopy(observation))
# Infer "task" from sub-environments. # Infer "task" from attributes of environments.
# env.call() works with both SyncVectorEnv and AsyncVectorEnv. # TODO: works with SyncVectorEnv but not AsyncVectorEnv
observation["task"] = env.call("task") observation = add_envs_task(env, observation)
# Apply environment-specific preprocessing (e.g., LiberoProcessorStep for LIBERO) # Apply environment-specific preprocessing (e.g., LiberoProcessorStep for LIBERO)
observation = env_preprocessor(observation) observation = env_preprocessor(observation)
@@ -200,6 +201,11 @@ def rollout(
"You're likely using an older version of gymnasium (< 1.0). Please upgrade." "You're likely using an older version of gymnasium (< 1.0). Please upgrade."
) )
successes = final_info["is_success"].tolist() successes = final_info["is_success"].tolist()
elif "is_success" in info:
is_success = info["is_success"]
successes = (
is_success.tolist() if hasattr(is_success, "tolist") else [bool(is_success)] * env.num_envs
)
else: else:
successes = [False] * env.num_envs successes = [False] * env.num_envs
@@ -733,48 +739,34 @@ def eval_policy_all(
group_acc[group]["video_paths"].extend(paths) group_acc[group]["video_paths"].extend(paths)
overall["video_paths"].extend(paths) overall["video_paths"].extend(paths)
def _make_thread_policy(p: PreTrainedPolicy) -> PreTrainedPolicy:
"""Shallow copy sharing weight tensors, with independent per-thread state.
copy.copy() gives a new Python object whose _parameters dict is a shared
reference (same tensor storage, zero extra VRAM). reset() then rebinds
mutable state (action queues etc.) to fresh per-thread objects.
Note: does NOT work for ACT with temporal_ensemble_coeff — that policy's
reset() mutates a shared sub-object. Use max_parallel_tasks=1 for that config.
"""
thread_p = copy.copy(p)
thread_p.reset()
return thread_p
# Choose runner (sequential vs threaded) # Choose runner (sequential vs threaded)
_runner_kwargs = { task_runner = partial(
"env_preprocessor": env_preprocessor, run_one,
"env_postprocessor": env_postprocessor, policy=policy,
"preprocessor": preprocessor, env_preprocessor=env_preprocessor,
"postprocessor": postprocessor, env_postprocessor=env_postprocessor,
"n_episodes": n_episodes, preprocessor=preprocessor,
"max_episodes_rendered": max_episodes_rendered, postprocessor=postprocessor,
"videos_dir": videos_dir, n_episodes=n_episodes,
"return_episode_data": return_episode_data, max_episodes_rendered=max_episodes_rendered,
"start_seed": start_seed, videos_dir=videos_dir,
} return_episode_data=return_episode_data,
start_seed=start_seed,
)
if max_parallel_tasks <= 1: if max_parallel_tasks <= 1:
# sequential path (single accumulator path on the main thread) # sequential path (single accumulator path on the main thread)
# NOTE: keeping a single-threaded accumulator avoids concurrent list appends or locks # NOTE: keeping a single-threaded accumulator avoids concurrent list appends or locks
for task_group, task_id, env in tasks: for task_group, task_id, env in tasks:
tg, tid, metrics = run_one(task_group, task_id, env, policy=policy, **_runner_kwargs) tg, tid, metrics = task_runner(task_group, task_id, env)
_accumulate_to(tg, metrics) _accumulate_to(tg, metrics)
per_task_infos.append({"task_group": tg, "task_id": tid, "metrics": metrics}) per_task_infos.append({"task_group": tg, "task_id": tid, "metrics": metrics})
else: else:
# threaded path: each thread gets a shallow policy copy (shared weights, independent state) # threaded path: submit all tasks, consume completions on main thread and accumulate there
with cf.ThreadPoolExecutor(max_workers=max_parallel_tasks) as executor: with cf.ThreadPoolExecutor(max_workers=max_parallel_tasks) as executor:
fut2meta = {} fut2meta = {}
for task_group, task_id, env in tasks: for task_group, task_id, env in tasks:
fut = executor.submit( fut = executor.submit(task_runner, task_group, task_id, env)
run_one, task_group, task_id, env, policy=_make_thread_policy(policy), **_runner_kwargs
)
fut2meta[fut] = (task_group, task_id) fut2meta[fut] = (task_group, task_id)
for fut in cf.as_completed(fut2meta): for fut in cf.as_completed(fut2meta):
tg, tid, metrics = fut.result() tg, tid, metrics = fut.result()

View File

@@ -421,6 +421,7 @@ def record_loop(
act_processed_policy: RobotAction = make_robot_action(action_values, dataset.features) act_processed_policy: RobotAction = make_robot_action(action_values, dataset.features)
# Applies a pipeline to the action, default is IdentityProcessor # Applies a pipeline to the action, default is IdentityProcessor
robot_action_to_send = robot_action_processor((act_processed_policy, obs)) robot_action_to_send = robot_action_processor((act_processed_policy, obs))
action_values = robot_action_to_send
elif policy is None and isinstance(teleop, Teleoperator): elif policy is None and isinstance(teleop, Teleoperator):
act = teleop.get_action() act = teleop.get_action()

View File

@@ -24,6 +24,7 @@ import torch
from huggingface_hub import HfApi from huggingface_hub import HfApi
from PIL import Image from PIL import Image
from safetensors.torch import load_file from safetensors.torch import load_file
from torchvision.transforms import v2
import lerobot import lerobot
from lerobot.configs.default import DatasetConfig from lerobot.configs.default import DatasetConfig
@@ -34,6 +35,7 @@ from lerobot.datasets.image_writer import image_array_to_pil_image
from lerobot.datasets.io_utils import hf_transform_to_torch from lerobot.datasets.io_utils import hf_transform_to_torch
from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.lerobot_dataset import LeRobotDataset
from lerobot.datasets.multi_dataset import MultiLeRobotDataset from lerobot.datasets.multi_dataset import MultiLeRobotDataset
from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig
from lerobot.datasets.utils import ( from lerobot.datasets.utils import (
DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_SIZE,
DEFAULT_DATA_FILE_SIZE_IN_MB, DEFAULT_DATA_FILE_SIZE_IN_MB,
@@ -355,6 +357,62 @@ def test_add_frame_image_pil(image_dataset):
assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW) assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW)
def test_set_image_transforms_applies_transparently(image_dataset):
dataset = image_dataset
dataset.add_frame({"image": np.random.rand(*DUMMY_CHW), "task": "Dummy task"})
dataset.save_episode()
dataset.finalize()
dataset.set_image_transforms(v2.Resize((224, 224)))
assert dataset[0]["image"].shape == torch.Size((3, 224, 224))
dataset.set_image_transforms(v2.Resize((128, 128)))
assert dataset[0]["image"].shape == torch.Size((3, 128, 128))
dataset.clear_image_transforms()
assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW)
def test_set_image_transforms_supports_lerobot_image_transforms(image_dataset):
dataset = image_dataset
dataset.add_frame({"image": np.random.rand(*DUMMY_CHW), "task": "Dummy task"})
dataset.save_episode()
dataset.finalize()
image_transforms = ImageTransforms(ImageTransformsConfig(enable=False))
dataset.set_image_transforms(image_transforms)
assert dataset.image_transforms is image_transforms
assert dataset[0]["image"].shape == torch.Size(DUMMY_CHW)
def test_set_image_transforms_supports_loaded_dataset(tmp_path, lerobot_dataset_factory):
dataset = lerobot_dataset_factory(root=tmp_path / "test", use_videos=False)
dataset.set_image_transforms(v2.Compose([v2.Resize((224, 224)), v2.Resize((112, 112))]))
camera_key = dataset.meta.camera_keys[0]
assert dataset[0][camera_key].shape == torch.Size((3, 112, 112))
def test_multilerobot_dataset_set_image_transforms_propagates(tmp_path, lerobot_dataset_factory):
root = tmp_path / "multi"
repo_ids = ["lerobot/test_multi_a", "lerobot/test_multi_b"]
for repo_id in repo_ids:
lerobot_dataset_factory(root=root / repo_id, repo_id=repo_id, use_videos=False)
dataset = MultiLeRobotDataset(repo_ids, root=root, download_videos=False)
dataset.set_image_transforms(v2.Resize((96, 96)))
camera_key = dataset.camera_keys[0]
assert dataset[0][camera_key].shape == torch.Size((3, 96, 96))
assert all(child.image_transforms is dataset.image_transforms for child in dataset._datasets)
dataset.clear_image_transforms()
assert dataset.image_transforms is None
assert all(child.image_transforms is None for child in dataset._datasets)
def test_image_array_to_pil_image_wrong_range_float_0_255(): def test_image_array_to_pil_image_wrong_range_float_0_255():
image = np.random.rand(*DUMMY_HWC) * 255 image = np.random.rand(*DUMMY_HWC) * 255
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@@ -22,6 +22,8 @@ def test_registry_all_types():
assert len(known) >= 6 assert len(known) >= 6
for t in known: for t in known:
cfg = make_env_config(t) cfg = make_env_config(t)
if not isinstance(cfg, EnvConfig):
continue
assert cfg.type == t assert cfg.type == t
@@ -54,10 +56,8 @@ def test_delegation():
def test_processors_delegation(): def test_processors_delegation():
"""make_env_pre_post_processors delegates to cfg.get_env_processors().""" """make_env_pre_post_processors delegates to cfg.get_env_processors()."""
from lerobot.configs.policies import PreTrainedConfig
cfg = make_env_config("aloha") cfg = make_env_config("aloha")
pre, post = make_env_pre_post_processors(cfg, PreTrainedConfig()) pre, post = make_env_pre_post_processors(cfg, policy_cfg=None)
assert len(pre.steps) == 0 assert len(pre.steps) == 0
@@ -124,7 +124,7 @@ def test_custom_create_envs_override():
def test_custom_get_env_processors_override(): def test_custom_get_env_processors_override():
"""A custom EnvConfig subclass can override get_env_processors().""" """A custom EnvConfig subclass can override get_env_processors()."""
from lerobot.processor.pipeline import PolicyProcessorPipeline from lerobot.processor.pipeline import DataProcessorPipeline
@EnvConfig.register_subclass("_dispatch_proc_test") @EnvConfig.register_subclass("_dispatch_proc_test")
@dataclass @dataclass
@@ -137,7 +137,7 @@ def test_custom_get_env_processors_override():
return {} return {}
def get_env_processors(self): def get_env_processors(self):
return PolicyProcessorPipeline(steps=[]), PolicyProcessorPipeline(steps=[]) return DataProcessorPipeline(steps=[]), DataProcessorPipeline(steps=[])
pre, post = _Env().get_env_processors() pre, post = _Env().get_env_processors()
assert isinstance(pre, PolicyProcessorPipeline) assert isinstance(pre, DataProcessorPipeline)

6050
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff