Compare commits

..

3 Commits

Author SHA1 Message Date
Nikodem Bartnik 71848ebb2e first text draft (no images) 2026-05-30 13:17:17 +02:00
Haoming Song 5c98e80430 fix(gr00t): fix Eagle25VL model and processor crash in transformers>=5.4.0, <5.6.0 (#3652)
Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
2026-05-26 14:04:22 +02:00
Reece O'Mahoney f65f3f7a4a Fix policy.path in YAML configs (PR #3145 followup) (#3597)
PR #3145 added YAML support for policy.path but left two bugs:

1. extract_path_fields_from_config only deleted config_data[field] when
   no sibling overrides existed. With siblings, the dict stayed in place
   and draccus crashed decoding it as PreTrainedConfig (no 'type' key).
   Sibling overrides go into _config_yaml_overrides and are applied later
   by from_pretrained(), so the field can always be removed.

2. wrap() updated config_path_cli to the cleaned temp file path but
   never propagated it to the draccus.parse fallback branch. cli_args
   still contained --config_path=<original>, so draccus read the
   original YAML with path: still present.

Tests passed because they (a) called extract_path_fields_from_config
directly and (b) included type: alongside path: in the YAML, sidestepping
both bugs.

Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
2026-05-26 14:01:19 +02:00
8 changed files with 167 additions and 23 deletions
-11
View File
@@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
actions:
patterns: ["*"]
+2
View File
@@ -9,6 +9,8 @@
- sections: - sections:
- local: il_robots - local: il_robots
title: Imitation Learning for Robots title: Imitation Learning for Robots
- local: lelab
title: LeLab - Lerobot GUI
- local: bring_your_own_policies - local: bring_your_own_policies
title: Adding a Policy title: Adding a Policy
- local: integrate_hardware - local: integrate_hardware
+42
View File
@@ -0,0 +1,42 @@
# LeLab - LeRobot Guide
Graphical user interfaces are the easiest to use for beginners because it's easy to just click everything without remembering the proper commands. That's why we built LeLab which is a GUI built on top of the LeRobot library. With this app you will be able to add robots, collect datasets, train and deploy models.
### Installation
To install lerobot you can simply copy the following command and paste into your terminal. For it to work you need to have `uv` installed, [here is how to do it.](https://docs.astral.sh/uv/getting-started/installation/)
```
uv tool install git+https://github.com/huggingface/leLab.git && lelab
```
Once installed you will be able to run lelab anytime you want with `lelab` command from your terminal (above command has it included at the end so it will run it right after installation).
### Adding robots
##### Calibration
You will need to select the proper arm type (leader or follower) and calibrate each arm as shown in the video available inside LeLab. Make sure that all joints are in the middle position when starting the calibration.
##### Adding cameras
At the bottom of the add robot page you can also add the cameras and name them accordingly.
### Teleoperation
Once the robots have been configured you can go back and click the teleoperation button. You will see the 3D visualization of the arm and will be able to control the follower with the leader. If something doesn't work there, remove and add your robot again following the steps described in LeLab.
### Recording a dataset
Type a new name for your dataset and press on the plus button. You will need to provide:
- Task description, for example "put the cube in a container"
- Number of episodes that you want to record, at least 30 recommended
- Episode and reset durations. These are max durations and can be shortened while recording with a spacebar press.
- If you configured your cameras earlier you don't need to do that again.
Press start recording, wait for it to load, perform the task with confident movements but don't rush. Once the task is finished and you moved your robot to the initial position press the spacebar. You will have time to reset the environment for example grab the cube from the container and placing it on the desk again. Once ready press the spacebar and record the next episode. Repeat until all the episodes are recorded.
### Training a model
This is the most powerful function with LeLab! You can easily train models locally on your own computer but also with [HF Jobs](https://huggingface.co/docs/huggingface_hub/en/guides/jobs) which gives you easy access to very powerful GPUs with clear pricing.
> [!TIP]
> To use HF Jobs make sure that you are logged in to HF, you can do that by running `hf auth login` in the terminal.
In the training tab select if you want to train locally or specific HF hardware you want to use. You will also need to provide the dataset that will be used for training. Your own datasets will be listed in a dropdown list, you can also use other datasets by providing its id. Set the policy you want to train, batch size and number of steps. For guide on choosing hardware and batch size check out our [Compute HW Guide for LeRobot Training.](hardware_guide.mdx)
Once you start training the progress will be visualized inside LeLab. Checkpoints will be saved as well.
### Running the model on a robot
In the main view of the LeLab under jobs you will see all the models that you trained. To run the policy on the robot just click the green run button and press start inference. After loading the policy the robot should start solving the task that it learned during training.
+7 -2
View File
@@ -255,7 +255,6 @@ def extract_path_fields_from_config(config_path: str, path_fields: list[str]) ->
remaining = config_data[field] remaining = config_data[field]
if remaining: if remaining:
_config_yaml_overrides[field] = _flatten_to_cli_args(remaining) _config_yaml_overrides[field] = _flatten_to_cli_args(remaining)
else:
del config_data[field] del config_data[field]
modified = True modified = True
@@ -311,7 +310,13 @@ def wrap(config_path: Path | None = None) -> Callable[[F], F]:
cli_args = filter_arg("config_path", cli_args) cli_args = filter_arg("config_path", cli_args)
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args) cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
else: else:
cfg = draccus.parse(config_class=argtype, config_path=config_path, args=cli_args) if config_path_cli:
cli_args = filter_arg("config_path", cli_args)
cfg = draccus.parse(
config_class=argtype,
config_path=config_path_cli or config_path,
args=cli_args,
)
response = fn(cfg, *args, **kwargs) response = fn(cfg, *args, **kwargs)
return response return response
@@ -60,6 +60,7 @@ class Eagle25VLPreTrainedModel(PreTrainedModel):
"SiglipEncoderLayer", "SiglipEncoderLayer",
] ]
_skip_keys_device_placement = "past_key_values" _skip_keys_device_placement = "past_key_values"
_supports_flash_attn = True
_supports_flash_attn_2 = True _supports_flash_attn_2 = True
_supports_cache_class = True _supports_cache_class = True
_supports_static_cache = True _supports_static_cache = True
@@ -124,7 +124,6 @@ class Eagle25VLProcessor(ProcessorMixin):
"videos_kwargs", "videos_kwargs",
"text_kwargs", "text_kwargs",
] ]
image_processor_class = "AutoImageProcessor"
tokenizer_class = "AutoTokenizer" tokenizer_class = "AutoTokenizer"
def __init__( def __init__(
@@ -206,7 +206,11 @@ def _build_eagle_processor(tokenizer_assets_repo: str = DEFAULT_TOKENIZER_ASSETS
"Vendor files are copied during model creation. Create the policy/model first, " "Vendor files are copied during model creation. Create the policy/model first, "
"or call ensure_eagle_cache_ready() before building processors." "or call ensure_eagle_cache_ready() before building processors."
) )
proc = AutoProcessor.from_pretrained(str(cache_dir), trust_remote_code=True, use_fast=True) proc = AutoProcessor.from_pretrained(
str(cache_dir),
trust_remote_code=True,
fix_mistral_regex=False,
)
proc.tokenizer.padding_side = "left" proc.tokenizer.padding_side = "left"
return proc return proc
+109 -7
View File
@@ -1,10 +1,14 @@
"""Tests for policy.path support in YAML config files (issue #2957).""" """Tests for policy.path support in YAML config files (issue #2957)."""
import json import json
import sys
import tempfile import tempfile
from dataclasses import dataclass, field
from unittest.mock import patch
import yaml import yaml
from lerobot.configs import parser
from lerobot.configs.parser import ( from lerobot.configs.parser import (
_config_path_args, _config_path_args,
_config_yaml_overrides, _config_yaml_overrides,
@@ -16,7 +20,8 @@ from lerobot.configs.parser import (
def test_extract_path_fields_from_yaml(): def test_extract_path_fields_from_yaml():
"""Test that policy.path is extracted from a YAML config and removed.""" """Test that policy.path is extracted from a YAML config and the policy block
is removed entirely (siblings are captured separately as cli_overrides)."""
config = { config = {
"dataset": {"repo_id": "lerobot/pusht"}, "dataset": {"repo_id": "lerobot/pusht"},
"policy": {"type": "smolvla", "path": "lerobot/smolvla_base", "push_to_hub": False}, "policy": {"type": "smolvla", "path": "lerobot/smolvla_base", "push_to_hub": False},
@@ -26,26 +31,33 @@ def test_extract_path_fields_from_yaml():
config_path = f.name config_path = f.name
_config_path_args.clear() _config_path_args.clear()
_config_yaml_overrides.clear()
cleaned_path = extract_path_fields_from_config(config_path, ["policy"]) cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
# Path should be extracted and stored # Path should be extracted and stored
assert _config_path_args["policy"] == "lerobot/smolvla_base" assert _config_path_args["policy"] == "lerobot/smolvla_base"
# Cleaned config should not have the path field # Cleaned config should not have the policy block at all -- draccus must not
# try to decode it as PreTrainedConfig; the actual config comes from
# from_pretrained(path) with the captured overrides applied on top.
with open(cleaned_path) as f: with open(cleaned_path) as f:
cleaned = yaml.safe_load(f) cleaned = yaml.safe_load(f)
assert "path" not in cleaned["policy"] assert "policy" not in cleaned
assert cleaned["policy"]["type"] == "smolvla"
assert cleaned["policy"]["push_to_hub"] is False
# Original dataset should be untouched # Original dataset should be untouched
assert cleaned["dataset"]["repo_id"] == "lerobot/pusht" assert cleaned["dataset"]["repo_id"] == "lerobot/pusht"
# Sibling overrides (excluding type/path) captured for from_pretrained.
overrides = get_yaml_overrides("policy")
assert any("push_to_hub=false" in o for o in overrides)
_config_path_args.clear() _config_path_args.clear()
_config_yaml_overrides.clear()
def test_extract_path_fields_from_json(): def test_extract_path_fields_from_json():
"""Test that policy.path is extracted from a JSON config.""" """Test that policy.path is extracted from a JSON config and the policy
block is removed entirely."""
config = { config = {
"policy": {"type": "act", "path": "some/local/path"}, "policy": {"type": "act", "path": "some/local/path"},
} }
@@ -54,15 +66,17 @@ def test_extract_path_fields_from_json():
config_path = f.name config_path = f.name
_config_path_args.clear() _config_path_args.clear()
_config_yaml_overrides.clear()
cleaned_path = extract_path_fields_from_config(config_path, ["policy"]) cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
assert _config_path_args["policy"] == "some/local/path" assert _config_path_args["policy"] == "some/local/path"
with open(cleaned_path) as f: with open(cleaned_path) as f:
cleaned = json.load(f) cleaned = json.load(f)
assert "path" not in cleaned["policy"] assert "policy" not in cleaned
_config_path_args.clear() _config_path_args.clear()
_config_yaml_overrides.clear()
def test_extract_no_path_returns_original(): def test_extract_no_path_returns_original():
@@ -216,3 +230,91 @@ def test_flatten_nested_with_bools():
args = _flatten_to_cli_args(d) args = _flatten_to_cli_args(d)
assert "--optimizer.use_warmup=true" in args assert "--optimizer.use_warmup=true" in args
assert "--optimizer.lr=0.01" in args assert "--optimizer.lr=0.01" in args
def test_extract_removes_field_with_siblings_and_no_type():
"""Regression: when policy.path has siblings but no type:, the entire policy
block must still be removed from the cleaned config. Otherwise draccus tries
to decode the leftover dict as PreTrainedConfig and crashes on the missing
type discriminator.
"""
config = {
"dataset": {"repo_id": "lerobot/pusht"},
"policy": {
"path": "lerobot/smolvla_base",
"n_action_steps": 10,
"dtype": "bfloat16",
},
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump(config, f)
config_path = f.name
_config_path_args.clear()
_config_yaml_overrides.clear()
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
with open(cleaned_path) as f:
cleaned = yaml.safe_load(f) or {}
assert "policy" not in cleaned, "policy block should be fully removed when path is present"
assert cleaned["dataset"]["repo_id"] == "lerobot/pusht"
assert _config_path_args["policy"] == "lerobot/smolvla_base"
overrides = get_yaml_overrides("policy")
assert any("n_action_steps=10" in o for o in overrides)
assert any("dtype=bfloat16" in o for o in overrides)
_config_path_args.clear()
_config_yaml_overrides.clear()
@dataclass
class _DummyNested:
foo: int = 0
@dataclass
class _DummyConfig:
nested: _DummyNested = field(default_factory=_DummyNested)
other: str = "default"
@classmethod
def __get_path_fields__(cls):
return ["nested"]
def test_wrap_uses_cleaned_config_for_draccus_parse():
"""Regression: wrap() updates config_path_cli to point at the cleaned temp
file but must propagate that to the draccus.parse fallback branch. Without
the fix, cli_args still contains --config_path=<original> and draccus reads
the original YAML with `path:` still in it, crashing on the unknown field.
"""
config = {
"nested": {"path": "some/checkpoint", "foo": 42},
"other": "set-via-yaml",
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump(config, f)
config_path = f.name
_config_path_args.clear()
_config_yaml_overrides.clear()
captured: dict = {}
@parser.wrap()
def main(cfg: _DummyConfig) -> _DummyConfig:
captured["cfg"] = cfg
return cfg
with patch.object(sys, "argv", ["prog", f"--config_path={config_path}"]):
main()
assert captured["cfg"].other == "set-via-yaml"
assert _config_path_args["nested"] == "some/checkpoint"
# Cleaned config dropped `nested:` entirely; defaults stand for this wrapper
# class (a real PreTrainedConfig would now load the checkpoint and apply
# the captured yaml_overrides via from_pretrained()).
assert captured["cfg"].nested.foo == 0
_config_path_args.clear()
_config_yaml_overrides.clear()