Compare commits

..

1 Commits

Author SHA1 Message Date
CarolinePascal 8353f84f52 docs(configs): bring src/lerobot/configs/ to 100% docstring coverage
Documents every remaining public class/function across configs/: the
types.py feature/normalization enums and PolicyFeature, the
PreTrainedConfig and RewardModelConfig base classes (full Args: blocks,
abstract property/method contracts, from_pretrained overrides), the
TrainPipelineConfig and EvalPipelineConfig top-level pipeline configs,
default.py's DatasetConfig/WandBConfig/EvalConfig/PeftConfig/JobConfig,
dataset.py's DatasetRecordConfig, video.py's VideoEncoderConfig, the
accelerator.py ActivationCheckpointingMode enum, and parser.py's CLI
argument-parsing helpers.

Converts existing inline `#` field comments on config dataclasses to
machine-checked Args: blocks (matching each field against its actual
constructor signature), and corrects a couple of stale/misattributed
comments found along the way (EvalPipelineConfig's misplaced field
comment, PreTrainedConfig's phantom normalization_mapping field).

Expands docs/source/api/configs.mdx from 5 documented classes to the
module's full public surface. Adds lerobot.configs to
check_docstrings.py's MODULES_TO_CHECK ratchet and removes the
module's ruff D-ignore. Docstrings and docstring-format changes only;
no behavioral changes.
2026-08-07 11:57:10 +02:00
18 changed files with 667 additions and 530 deletions
+62
View File
@@ -10,14 +10,26 @@ itself with `@register_subclass("name")` and is then selectable by that name on
[[autodoc]] lerobot.configs.train.TrainPipelineConfig [[autodoc]] lerobot.configs.train.TrainPipelineConfig
## EvalPipelineConfig
[[autodoc]] lerobot.configs.eval.EvalPipelineConfig
## PreTrainedConfig ## PreTrainedConfig
[[autodoc]] lerobot.configs.PreTrainedConfig [[autodoc]] lerobot.configs.PreTrainedConfig
## RewardModelConfig
[[autodoc]] lerobot.configs.rewards.RewardModelConfig
## DatasetConfig ## DatasetConfig
[[autodoc]] lerobot.configs.DatasetConfig [[autodoc]] lerobot.configs.DatasetConfig
## DatasetRecordConfig
[[autodoc]] lerobot.configs.DatasetRecordConfig
## EvalConfig ## EvalConfig
[[autodoc]] lerobot.configs.EvalConfig [[autodoc]] lerobot.configs.EvalConfig
@@ -25,3 +37,53 @@ itself with `@register_subclass("name")` and is then selectable by that name on
## WandBConfig ## WandBConfig
[[autodoc]] lerobot.configs.WandBConfig [[autodoc]] lerobot.configs.WandBConfig
## PeftConfig
[[autodoc]] lerobot.configs.PeftConfig
## JobConfig
[[autodoc]] lerobot.configs.JobConfig
## Feature types
[[autodoc]] lerobot.configs.FeatureType
[[autodoc]] lerobot.configs.PipelineFeatureType
[[autodoc]] lerobot.configs.NormalizationMode
[[autodoc]] lerobot.configs.PolicyFeature
[[autodoc]] lerobot.configs.RTCAttentionSchedule
## Video encoding
[[autodoc]] lerobot.configs.VideoEncoderConfig
[[autodoc]] lerobot.configs.RGBEncoderConfig
[[autodoc]] lerobot.configs.DepthEncoderConfig
[[autodoc]] lerobot.configs.encoder_config_from_video_info
## Distributed training
[[autodoc]] lerobot.configs.parallelism.ParallelismConfig
[[autodoc]] lerobot.configs.parallelism.ContextParallelConfig
[[autodoc]] lerobot.configs.accelerator.AcceleratorConfig
[[autodoc]] lerobot.configs.accelerator.FSDPConfig
[[autodoc]] lerobot.configs.accelerator.DDPConfig
[[autodoc]] lerobot.configs.accelerator.GradientAccumulationConfig
[[autodoc]] lerobot.configs.accelerator.CompileConfig
[[autodoc]] lerobot.configs.accelerator.ActivationCheckpointingConfig
[[autodoc]] lerobot.configs.accelerator.ActivationCheckpointingMode
-93
View File
@@ -1,93 +0,0 @@
# Optimization
`OptimizerConfig` and `LRSchedulerConfig` are the base configuration classes for the optimizers and learning
rate schedulers used during training. `TrainPipelineConfig` composes one of each; see
[`~optim.factory.make_optimizer_and_scheduler`] for how they are built from a policy's parameters.
## make_optimizer_and_scheduler
[[autodoc]] lerobot.optim.factory.make_optimizer_and_scheduler
## OptimizerConfig
[[autodoc]] lerobot.optim.OptimizerConfig
- type
- builds_multiple_optimizers
- default_choice_name
- build
## AdamConfig
[[autodoc]] lerobot.optim.AdamConfig
- build
## AdamWConfig
[[autodoc]] lerobot.optim.AdamWConfig
- build
## SGDConfig
[[autodoc]] lerobot.optim.SGDConfig
- build
## MultiAdamConfig
Builds a dictionary of Adam optimizers, one per parameter group — used when a policy needs separate
optimizers for different components (e.g. actor/critic/temperature in SAC).
[[autodoc]] lerobot.optim.MultiAdamConfig
- builds_multiple_optimizers
- build
## XVLAAdamWConfig
[[autodoc]] lerobot.optim.XVLAAdamWConfig
- build
## save_optimizer_state
[[autodoc]] lerobot.optim.save_optimizer_state
## load_optimizer_state
[[autodoc]] lerobot.optim.load_optimizer_state
## LRSchedulerConfig
[[autodoc]] lerobot.optim.LRSchedulerConfig
- type
- build
## DiffuserSchedulerConfig
[[autodoc]] lerobot.optim.DiffuserSchedulerConfig
- build
## VQBeTSchedulerConfig
[[autodoc]] lerobot.optim.VQBeTSchedulerConfig
- build
## ConstantWithWarmupSchedulerConfig
[[autodoc]] lerobot.optim.schedulers.ConstantWithWarmupSchedulerConfig
- build
## CosineAnnealingWithWarmupSchedulerConfig
[[autodoc]] lerobot.optim.schedulers.CosineAnnealingWithWarmupSchedulerConfig
- build
## CosineDecayWithWarmupSchedulerConfig
[[autodoc]] lerobot.optim.CosineDecayWithWarmupSchedulerConfig
- build
## save_scheduler_state
[[autodoc]] lerobot.optim.save_scheduler_state
## load_scheduler_state
[[autodoc]] lerobot.optim.load_scheduler_state
+1 -1
View File
@@ -439,7 +439,6 @@ ignore = [
"src/lerobot/async_inference/**" = ["D"] "src/lerobot/async_inference/**" = ["D"]
"src/lerobot/cameras/**" = ["D"] "src/lerobot/cameras/**" = ["D"]
"src/lerobot/common/**" = ["D"] "src/lerobot/common/**" = ["D"]
"src/lerobot/configs/**" = ["D"]
"src/lerobot/data_processing/**" = ["D"] "src/lerobot/data_processing/**" = ["D"]
"src/lerobot/datasets/**" = ["D"] "src/lerobot/datasets/**" = ["D"]
"src/lerobot/distributed/**" = ["D"] "src/lerobot/distributed/**" = ["D"]
@@ -447,6 +446,7 @@ ignore = [
"src/lerobot/jobs/**" = ["D"] "src/lerobot/jobs/**" = ["D"]
"src/lerobot/model/**" = ["D"] "src/lerobot/model/**" = ["D"]
"src/lerobot/motors/**" = ["D"] "src/lerobot/motors/**" = ["D"]
"src/lerobot/optim/**" = ["D"]
"src/lerobot/policies/**" = ["D"] "src/lerobot/policies/**" = ["D"]
"src/lerobot/processor/**" = ["D"] "src/lerobot/processor/**" = ["D"]
"src/lerobot/rewards/**" = ["D"] "src/lerobot/rewards/**" = ["D"]
+1 -2
View File
@@ -12,8 +12,7 @@
# 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.
""" """Public API for lerobot configuration types and base config classes.
Public API for lerobot configuration types and base config classes.
NOTE: TrainPipelineConfig, EvalPipelineConfig, and TrainRLServerPipelineConfig NOTE: TrainPipelineConfig, EvalPipelineConfig, and TrainRLServerPipelineConfig
are intentionally NOT re-exported here to avoid circular dependencies are intentionally NOT re-exported here to avoid circular dependencies
+7
View File
@@ -171,6 +171,13 @@ class CompileConfig:
class ActivationCheckpointingMode(str, Enum): class ActivationCheckpointingMode(str, Enum):
"""The activation-checkpointing strategy applied to FSDP wrap units.
**Attributes**:
- **NONE** -- No activation checkpointing.
- **FULL** -- Checkpoint every wrap unit.
"""
NONE = "none" NONE = "none"
FULL = "full" FULL = "full"
+51 -31
View File
@@ -23,56 +23,76 @@ from .video import DepthEncoderConfig, RGBEncoderConfig, depth_encoder_defaults,
@dataclass @dataclass
class DatasetRecordConfig: class DatasetRecordConfig:
# Dataset identifier. By convention it should match '{hf_username}/{dataset_name}' (e.g. `lerobot/test`). """Shared dataset recording configuration used by both `lerobot-record` and `lerobot-rollout`.
Args:
repo_id (`str`, *optional*, defaults to `""`): Dataset identifier. By convention it should match
`'{hf_username}/{dataset_name}'` (e.g. `lerobot/test`).
single_task (`str`, *optional*, defaults to `""`): A short but accurate description of the task performed during the
recording (e.g. `"Pick the Lego block and drop it in the box on the right."`).
root (`str | Path | None`, *optional*): Root directory where the dataset will be stored (e.g.
`'dataset/path'`). If `None`, defaults to `$HF_LEROBOT_HOME/repo_id`.
fps (`int`, *optional*, defaults to 30): Limit the frames per second.
episode_time_s (`int | float`, *optional*, defaults to 60): Number of seconds for data recording
for each episode.
reset_time_s (`int | float`, *optional*, defaults to 60): Number of seconds for resetting the
environment after each episode.
num_episodes (`int`, *optional*, defaults to 50): Number of episodes to record.
video (`bool`, *optional*, defaults to `True`): Encode frames in the dataset into video.
push_to_hub (`bool`, *optional*, defaults to `True`): Upload dataset to the Hugging Face Hub.
private (`bool | None`, *optional*): If `True`, upload as private; if `None`, defer to the org
default on the Hub (only affects orgs).
tags (`list[str] | None`, *optional*): Add tags to your dataset on the Hub.
num_image_writer_processes (`int`, *optional*, defaults to 0): Number of subprocesses handling the
saving of frames as PNG. Set to 0 to use threads only; set to >=1 to use subprocesses, each
using threads to write images. The best number of processes and threads depends on your
system. We recommend 4 threads per camera with 0 processes. If fps is unstable, adjust the
thread count. If still unstable, try using 1 or more subprocesses.
num_image_writer_threads_per_camera (`int`, *optional*, defaults to 4): Number of threads writing
the frames as png images on disk, per camera. Too many threads might cause unstable
teleoperation fps due to the main thread being blocked. Not enough threads might cause low
camera fps.
video_encoding_batch_size (`int`, *optional*, defaults to 1): Number of episodes to record before
batch encoding videos. Set to 1 for immediate encoding (default behavior), or higher for
batched encoding.
rgb_encoder (`RGBEncoderConfig`, *optional*): Video encoder settings for camera MP4s (codec,
quality, GOP, etc.). Tuned via CLI nested keys, e.g. `--dataset.rgb_encoder.vcodec=h264`.
depth_encoder (`DepthEncoderConfig`, *optional*): Video encoder settings for depth-map MP4s (codec,
quality, GOP, etc.). Tuned via CLI nested keys.
streaming_encoding (`bool`, *optional*, defaults to `False`): Enable streaming video encoding:
encode frames in real-time during capture instead of writing PNG images first. Makes
`save_episode()` near-instant. More info in the documentation:
https://huggingface.co/docs/lerobot/streaming_video_encoding
encoder_queue_maxsize (`int`, *optional*, defaults to 30): Maximum number of frames to buffer per
camera when using streaming encoding. ~1s buffer at 30fps. Provides backpressure if the encoder
can't keep up.
encoder_threads (`int | None`, *optional*): Number of threads per encoder instance. `None` means
auto (codec default). Lower values reduce CPU usage; maps to `'lp'` (via `svtav1-params`) for
libsvtav1 and `'threads'` for h264/hevc.
no_stamp (`bool`, *optional*, defaults to `False`): Skip appending the date-time tag to `repo_id`,
keeping the user-provided name as-is (e.g. self-managed versioned names intended for a later
`lerobot-edit-dataset merge`).
"""
repo_id: str = "" repo_id: str = ""
# A short but accurate description of the task performed during the recording (e.g. "Pick the Lego block and drop it in the box on the right.")
single_task: str = "" single_task: str = ""
# Root directory where the dataset will be stored (e.g. 'dataset/path'). If None, defaults to $HF_LEROBOT_HOME/repo_id.
root: str | Path | None = None root: str | Path | None = None
# Limit the frames per second.
fps: int = 30 fps: int = 30
# Number of seconds for data recording for each episode.
episode_time_s: int | float = 60 episode_time_s: int | float = 60
# Number of seconds for resetting the environment after each episode.
reset_time_s: int | float = 60 reset_time_s: int | float = 60
# Number of episodes to record.
num_episodes: int = 50 num_episodes: int = 50
# Encode frames in the dataset into video
video: bool = True video: bool = True
# Upload dataset to Hugging Face hub.
push_to_hub: bool = True push_to_hub: bool = True
# If True, upload as private; if None, defer to the org default on the Hub (only affects orgs).
private: bool | None = None private: bool | None = None
# Add tags to your dataset on the hub.
tags: list[str] | None = None tags: list[str] | None = None
# Number of subprocesses handling the saving of frames as PNG. Set to 0 to use threads only;
# set to ≥1 to use subprocesses, each using threads to write images. The best number of processes
# and threads depends on your system. We recommend 4 threads per camera with 0 processes.
# If fps is unstable, adjust the thread count. If still unstable, try using 1 or more subprocesses.
num_image_writer_processes: int = 0 num_image_writer_processes: int = 0
# Number of threads writing the frames as png images on disk, per camera.
# Too many threads might cause unstable teleoperation fps due to main thread being blocked.
# Not enough threads might cause low camera fps.
num_image_writer_threads_per_camera: int = 4 num_image_writer_threads_per_camera: int = 4
# Number of episodes to record before batch encoding videos
# Set to 1 for immediate encoding (default behavior), or higher for batched encoding
video_encoding_batch_size: int = 1 video_encoding_batch_size: int = 1
# Video encoder settings for camera MP4s (codec, quality, GOP, etc.). Tuned via CLI nested keys,
# e.g. ``--dataset.rgb_encoder.vcodec=h264`` (see ``RGBEncoderConfig``).
rgb_encoder: RGBEncoderConfig = field(default_factory=rgb_encoder_defaults) rgb_encoder: RGBEncoderConfig = field(default_factory=rgb_encoder_defaults)
# Video encoder settings for depth-map MP4s (codec, quality, GOP, etc.). Tuned via CLI nested keys.
depth_encoder: DepthEncoderConfig = field(default_factory=depth_encoder_defaults) depth_encoder: DepthEncoderConfig = field(default_factory=depth_encoder_defaults)
# Enable streaming video encoding: encode frames in real-time during capture instead
# of writing PNG images first. Makes save_episode() near-instant. More info in the documentation: https://huggingface.co/docs/lerobot/streaming_video_encoding
streaming_encoding: bool = False streaming_encoding: bool = False
# Maximum number of frames to buffer per camera when using streaming encoding.
# ~1s buffer at 30fps. Provides backpressure if the encoder can't keep up.
encoder_queue_maxsize: int = 30 encoder_queue_maxsize: int = 30
# Number of threads per encoder instance. None = auto (codec default).
# Lower values reduce CPU usage, maps to 'lp' (via svtav1-params) for libsvtav1 and 'threads' for h264/hevc..
encoder_threads: int | None = None encoder_threads: int | None = None
# Skip appending the date-time tag to repo_id, keeping the user-provided name as-is
# (e.g. self-managed versioned names intended for a later `lerobot-edit-dataset merge`).
no_stamp: bool = False no_stamp: bool = False
def stamp_repo_id(self) -> None: def stamp_repo_id(self) -> None:
+128 -58
View File
@@ -27,35 +27,65 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class DatasetConfig: class DatasetConfig:
# You may provide a list of datasets here. `train.py` creates them all and concatenates them. Note: only data """A dataset to train on. `TrainPipelineConfig.dataset` may be a list of these, concatenated together.
# keys common between the datasets are kept. Each dataset gets and additional transform that inserts the
# "dataset_index" into the returned item. The index mapping is made according to the order in which the Only data keys common between multiple datasets are kept. Each dataset gets an additional transform
# datasets are provided. that inserts the `"dataset_index"` into the returned item, with the index mapping made according to
the order in which the datasets are provided.
Args:
repo_id (`str`): The Hub repo ID (or local dataset name, if `root` is set) to load.
repo_type (`str`, *optional*, defaults to `"dataset"`): Hub repository type: `"dataset"` (the
default) or `"bucket"` for an HF Storage Bucket streamed over `hf://buckets/`. Buckets are
streaming-only, so `"bucket"` requires `streaming=True`.
root (`str | None`, *optional*): Root directory for a concrete local dataset tree (e.g.
`'dataset/path'`). If `None`, local datasets are looked up under `$HF_LEROBOT_HOME/repo_id` and
Hub downloads use a revision-safe cache under `$HF_LEROBOT_HOME/hub`.
episodes (`list[int] | None`, *optional*): Episode indices to include. If `None`, all episodes are
used.
exclude_episodes (`list[int] | None`, *optional*): Episode indices to drop (e.g. corrupt or
heterogeneous ones). Applied on top of `episodes`.
image_transforms (`ImageTransformsConfig`, *optional*): Image augmentation settings applied at load
time.
revision (`str | None`, *optional*): Hub revision (commit hash, branch, or tag) to load.
use_imagenet_stats (`bool`, *optional*, defaults to `True`): Whether to use ImageNet normalization
statistics for visual features instead of the dataset's own.
video_backend (`str`, *optional*): The video decoding backend to use.
return_uint8 (`bool`, *optional*, defaults to `False`): When `True`, RGB video frames are returned
as `uint8` tensors (0-255) instead of `float32` (0.0-1.0). This reduces memory and speeds up
DataLoader IPC. The training pipeline handles the conversion.
depth_output_unit (`str`, *optional*, defaults to `"mm"`): Physical unit depth maps are dequantized
to at load time: `"mm"` (millimeters) or `"m"` (metres). Has no effect on datasets without depth
cameras.
streaming (`bool`, *optional*, defaults to `False`): Stream the dataset instead of downloading it
locally.
eval_split (`float`, *optional*, defaults to 0.0): Fraction of episodes held out per task for
offline evaluation (0.0 = disabled).
"""
repo_id: str repo_id: str
# Hub repository type: "dataset" (default) or "bucket" for an HF Storage Bucket streamed over
# hf://buckets/. Buckets are streaming-only, so "bucket" requires streaming=true.
repo_type: str = "dataset" repo_type: str = "dataset"
# Root directory for a concrete local dataset tree (e.g. 'dataset/path'). If None, local datasets are
# looked up under $HF_LEROBOT_HOME/repo_id and Hub downloads use a revision-safe cache under $HF_LEROBOT_HOME/hub.
root: str | None = None root: str | None = None
episodes: list[int] | None = None episodes: list[int] | None = None
# Episode indices to drop (e.g. corrupt or heterogeneous ones). Applied on top of `episodes`.
exclude_episodes: list[int] | None = None exclude_episodes: list[int] | None = None
image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig) image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig)
revision: str | None = None revision: str | None = None
use_imagenet_stats: bool = True use_imagenet_stats: bool = True
video_backend: str = field(default_factory=get_safe_default_video_backend) video_backend: str = field(default_factory=get_safe_default_video_backend)
# When True, RGB video frames are returned as uint8 tensors (0-255) instead of float32 (0.0-1.0).
# This reduces memory and speeds up DataLoader IPC. The training pipeline handles the conversion.
return_uint8: bool = False return_uint8: bool = False
# Physical unit depth maps are dequantized to at load time: "mm" (millimeters) or "m" (metres).
# Has no effect on datasets without depth cameras.
depth_output_unit: str = DEFAULT_DEPTH_UNIT depth_output_unit: str = DEFAULT_DEPTH_UNIT
streaming: bool = False streaming: bool = False
# Fraction of episodes held out per task for offline evaluation (0.0 = disabled).
eval_split: float = 0.0 eval_split: float = 0.0
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Validate `repo_type`/`streaming`/`depth_output_unit`/`eval_split`/`episodes`/`exclude_episodes`.
Raises:
ValueError: If `repo_type` isn't `"dataset"` or `"bucket"`; if `repo_type="bucket"` is combined
with `streaming=False` or a nonzero `eval_split`; if `depth_output_unit` isn't a recognized
unit; if `eval_split` is outside `[0.0, 1.0)`; or if `episodes` contains negative or
duplicate indices.
"""
if self.repo_type not in ("dataset", "bucket"): if self.repo_type not in ("dataset", "bucket"):
raise ValueError(f"repo_type must be 'dataset' or 'bucket', got {self.repo_type!r}") raise ValueError(f"repo_type must be 'dataset' or 'bucket', got {self.repo_type!r}")
if self.repo_type == "bucket" and not self.streaming: if self.repo_type == "bucket" and not self.streaming:
@@ -92,35 +122,63 @@ class DatasetConfig:
@dataclass @dataclass
class WandBConfig: class WandBConfig:
"""Weights & Biases logging settings for `lerobot-train`.
Args:
enable (`bool`, *optional*, defaults to `False`): Whether to log this run to Weights & Biases.
disable_artifact (`bool`, *optional*, defaults to `False`): Set to `True` to disable saving an
artifact despite `save_checkpoint=True`.
project (`str`, *optional*, defaults to `"lerobot"`): The WandB project to log to.
entity (`str | None`, *optional*): The WandB entity (team or username) to log under.
notes (`str | None`, *optional*): Notes attached to the WandB run.
run_id (`str | None`, *optional*): An existing WandB run id to resume logging into.
mode (`str | None`, *optional*): WandB mode: `"online"`, `"offline"`, or `"disabled"`. Defaults to
`"online"`.
add_tags (`bool`, *optional*, defaults to `True`): If `True`, save the training configuration as
tags on the WandB run.
"""
enable: bool = False enable: bool = False
# Set to true to disable saving an artifact despite training.save_checkpoint=True
disable_artifact: bool = False disable_artifact: bool = False
project: str = "lerobot" project: str = "lerobot"
entity: str | None = None entity: str | None = None
notes: str | None = None notes: str | None = None
run_id: str | None = None run_id: str | None = None
mode: str | None = None # Allowed values: 'online', 'offline' 'disabled'. Defaults to 'online' mode: str | None = None
add_tags: bool = True # If True, save configuration as tags in the WandB run. add_tags: bool = True
@dataclass @dataclass
class EvalConfig: class EvalConfig:
"""Settings for the periodic in-training simulation-environment evaluation.
Args:
n_episodes (`int`, *optional*, defaults to 50): Number of episodes to run per evaluation.
batch_size (`int`, *optional*, defaults to 0): The number of environments to use in a
`gym.vector.VectorEnv`. `0` auto-tunes based on available CPU cores and `n_episodes`.
use_async_envs (`bool`, *optional*, defaults to `True`): Whether to use asynchronous environments
(multiprocessing). Automatically downgraded to a `SyncVectorEnv` when `batch_size` is 1.
recording (`bool`, *optional*, defaults to `False`): Whether to record eval rollouts as a LeRobot
dataset on disk.
recording_repo_id (`str | None`, *optional*): If set, push recorded eval datasets to the Hub under
this repo id (one repo per task, suffixed by task and env index). Requires `recording=True`.
recording_private (`bool`, *optional*, defaults to `False`): Whether the pushed recording
repositories should be private.
"""
n_episodes: int = 50 n_episodes: int = 50
# `batch_size` specifies the number of environments to use in a gym.vector.VectorEnv.
# Set to 0 for auto-tuning based on available CPU cores and n_episodes.
batch_size: int = 0 batch_size: int = 0
# `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 = True use_async_envs: bool = True
# Whether to record eval rollouts as a LeRobot dataset on disk.
recording: bool = False recording: bool = False
# If set, push recorded eval datasets to the Hub under this repo id (one repo per task,
# suffixed by task and env index). Requires recording=true.
recording_repo_id: str | None = None recording_repo_id: str | None = None
# Whether the pushed recording repositories should be private.
recording_private: bool = False recording_private: bool = False
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Validate `recording_repo_id`/`recording`, and resolve/cap `batch_size`.
Raises:
ValueError: If `recording_repo_id` is set without `recording=True`.
"""
if self.recording_repo_id is not None and not self.recording: if self.recording_repo_id is not None and not self.recording:
raise ValueError("eval.recording_repo_id requires eval.recording=true.") raise ValueError("eval.recording_repo_id requires eval.recording=true.")
if self.batch_size == 0: if self.batch_size == 0:
@@ -141,54 +199,66 @@ class EvalConfig:
@dataclass @dataclass
class PeftConfig: class PeftConfig:
# PEFT offers many fine-tuning methods, layer adapters being the most common and currently also the most """PEFT (parameter-efficient fine-tuning) settings, e.g. LoRA adapters.
# effective methods so we'll focus on those in this high-level config interface.
PEFT offers many fine-tuning methods, layer adapters being the most common and currently also the
most effective methods so we'll focus on those in this high-level config interface.
Args:
target_modules (`list[str] | str | None`, *optional*): Either a string (module name suffix or
`'all-linear'`), a list of module name suffixes, or a regular expression describing module
names to target with the configured PEFT method. Some policies have a default value for this
so that you don't *have* to choose which layers to adapt, but it might still be worthwhile
depending on your case.
full_training_modules (`list[str] | None`, *optional*): Names/suffixes of modules to fully
fine-tune and store alongside adapter weights. Useful for layers that are not part of a
pre-trained model (e.g., action state projections). Depending on the policy this defaults to
layers that are newly created in pre-trained policies. If you're fine-tuning an already trained
policy you might want to set this to `[]`. Corresponds to PEFT's `modules_to_save`.
method_type (`str`, *optional*, defaults to `"LORA"`): The PEFT (adapter) method to apply to the
policy. Needs to be a valid PEFT type.
init_type (`str | None`, *optional*): Adapter initialization method. Look at the specific PEFT
adapter documentation for defaults.
r (`int`, *optional*, defaults to 16): We expect that all PEFT adapters are in some way doing
rank-decomposition, therefore this parameter specifies the rank used for the adapter. In
general a higher rank means more trainable parameters and closer to full fine-tuning.
lora_alpha (`int | None`, *optional*): Alpha parameter for LoRA scaling (`scaling = lora_alpha /
r`). In general, a higher alpha means stronger adaptation signal. If `None`, the PEFT library
defaults to `alpha=8`, which may dampen high-rank adapters. Common values are `r` (`alpha ==
rank`) or `2*r`.
"""
# Either a string (module name suffix or 'all-linear'), a list of module name suffixes or a regular expression
# describing module names to target with the configured PEFT method. Some policies have a default value for this
# so that you don't *have* to choose which layers to adapt but it might still be worthwhile depending on your case.
target_modules: list[str] | str | None = None target_modules: list[str] | str | None = None
# Names/suffixes of modules to fully fine-tune and store alongside adapter weights. Useful for layers that are
# not part of a pre-trained model (e.g., action state projections). Depending on the policy this defaults to layers
# that are newly created in pre-trained policies. If you're fine-tuning an already trained policy you might want
# to set this to `[]`. Corresponds to PEFT's `modules_to_save`.
full_training_modules: list[str] | None = None full_training_modules: list[str] | None = None
# The PEFT (adapter) method to apply to the policy. Needs to be a valid PEFT type.
method_type: str = "LORA" method_type: str = "LORA"
# Adapter initialization method. Look at the specific PEFT adapter documentation for defaults.
init_type: str | None = None init_type: str | None = None
# We expect that all PEFT adapters are in some way doing rank-decomposition therefore this parameter specifies
# the rank used for the adapter. In general a higher rank means more trainable parameters and closer to full
# fine-tuning.
r: int = 16 r: int = 16
# Alpha parameter for LoRA scaling (scaling = lora_alpha / r).
# In general, a higher alpha means stronger adaptation signal.
# If None, the PEFT library defaults to alpha=8, which may dampen high-rank adapters.
# Common values are r (alpha == rank) or 2*r.
lora_alpha: int | None = None lora_alpha: int | None = None
@dataclass @dataclass
class JobConfig: class JobConfig:
# Where training runs. None (omitted) or "local" runs on this machine. """Where and how a training run executes: locally, or dispatched to an HF Jobs flavor.
# Any other value is an HF Jobs flavor and submits the run to HF Jobs.
# List available flavors + pricing with `hf jobs hardware` command. Args:
target (`str | None`, *optional*): Where training runs. `None` (omitted) or `"local"` runs on this
machine. Any other value is an HF Jobs flavor and submits the run to HF Jobs. List available
flavors and pricing with the `hf jobs hardware` command.
image (`str`, *optional*, defaults to `"huggingface/lerobot-gpu:latest"`): Runtime image for the
remote job (ignored for local runs).
timeout (`str | None`, *optional*, defaults to `"2d"`): Max wall-clock for the remote job as an HF
Jobs duration string (e.g. `"2h"`). HF Jobs itself defaults to `"2d"`; we pass an explicit,
generous cap instead. Set a smaller value to fail fast, or a larger one for long runs.
detach (`bool`, *optional*, defaults to `False`): Submit and exit instead of streaming the job logs
in the foreground.
tags (`list[str]`, *optional*): Extra tags attached to the HF job and to any dataset this run
pushes to the Hub. A `"lerobot"` tag is always added; e.g. `--job.tags '["lelab"]'` adds more.
"""
target: str | None = None target: str | None = None
# Runtime image for the remote job (ignored for local runs).
image: str = "huggingface/lerobot-gpu:latest" image: str = "huggingface/lerobot-gpu:latest"
# Max wall-clock for the remote job as an HF Jobs duration string (e.g. "2h").
# Defaults to "2d": We pass an explicit, generous cap instead. Set a smaller
# value to fail fast, or a larger one for long runs.
timeout: str | None = "2d" timeout: str | None = "2d"
# Submit and exit instead of streaming the job logs in the foreground.
detach: bool = False detach: bool = False
# Extra tags attached to the HF job and to any dataset this run pushes to the
# Hub. A "lerobot" tag is always added; e.g. --job.tags '["lelab"]' adds more.
tags: list[str] = field(default_factory=list) tags: list[str] = field(default_factory=list)
# Two entry points to the same predicate: the staticmethod tests a raw target string # Two entry points to the same predicate: the staticmethod tests a raw target string
+21 -6
View File
@@ -28,21 +28,36 @@ logger = getLogger(__name__)
@dataclass @dataclass
class EvalPipelineConfig: class EvalPipelineConfig:
# Either the repo ID of a model hosted on the Hub or a path to a directory containing weights """The top-level configuration for `lerobot-eval`, parsed by draccus from CLI flags and/or a YAML file.
# saved using `Policy.save_pretrained`. If not provided, the policy is initialized from scratch
# (useful for debugging). This argument is mutually exclusive with `--config`. Args:
env (`envs.EnvConfig`): The simulation environment to evaluate the policy in.
eval (`EvalConfig`, *optional*): Number of episodes, batching, and recording settings for the
evaluation run.
policy (`PreTrainedConfig | None`, *optional*): Loaded from `--policy.path`, either the repo ID of
a model hosted on the Hub or a path to a directory containing weights saved using
`PreTrainedPolicy.save_pretrained`. If not provided, the policy is initialized from scratch
(useful for debugging).
output_dir (`Path | None`, *optional*): Where to save evaluation outputs.
job_name (`str | None`, *optional*): A name for the run.
seed (`int | None`, *optional*, defaults to 1000): Seed used for the evaluation environments.
rename_map (`dict[str, str]`, *optional*): Rename map for the observation, to override the image
and state keys.
trust_remote_code (`bool`, *optional*, defaults to `False`): Explicit consent to execute remote
code from the Hub (required for Hub environments).
"""
env: envs.EnvConfig env: envs.EnvConfig
eval: EvalConfig = field(default_factory=EvalConfig) eval: EvalConfig = field(default_factory=EvalConfig)
policy: PreTrainedConfig | None = None policy: PreTrainedConfig | None = None
output_dir: Path | None = None output_dir: Path | None = None
job_name: str | None = None job_name: str | None = None
seed: int | None = 1000 seed: int | None = 1000
# Rename map for the observation to override the image and state keys
rename_map: dict[str, str] = field(default_factory=dict) rename_map: dict[str, str] = field(default_factory=dict)
# Explicit consent to execute remote code from the Hub (required for hub environments).
trust_remote_code: bool = False trust_remote_code: bool = False
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Resolve `--policy.path` into a loaded config, and derive `job_name`/`output_dir` when unset."""
# HACK: We parse again the cli args here to get the pretrained path if there was one. # HACK: We parse again the cli args here to get the pretrained path if there was one.
policy_path = parser.get_path_arg("policy") policy_path = parser.get_path_arg("policy")
if policy_path: if policy_path:
@@ -75,5 +90,5 @@ class EvalPipelineConfig:
@classmethod @classmethod
def __get_path_fields__(cls) -> list[str]: def __get_path_fields__(cls) -> list[str]:
"""This enables the parser to load config from the policy using `--policy.path=local/dir`""" """This enables the parser to load config from the policy using `--policy.path=local/dir`."""
return ["policy"] return ["policy"]
+30 -19
View File
@@ -96,6 +96,7 @@ def get_cli_overrides(field_name: str, args: Sequence[str] | None = None) -> lis
def parse_arg(arg_name: str, args: Sequence[str] | None = None) -> str | None: def parse_arg(arg_name: str, args: Sequence[str] | None = None) -> str | None:
"""Return the value of `--{arg_name}=value` or `--{arg_name} value` in `args` (`sys.argv[1:]` if `None`)."""
if args is None: if args is None:
args = sys.argv[1:] args = sys.argv[1:]
option = f"--{arg_name}" option = f"--{arg_name}"
@@ -115,7 +116,7 @@ def parse_plugin_args(plugin_arg_suffix: str, args: Sequence[str]) -> dict[str,
Args: Args:
plugin_arg_suffix (str): The suffix to identify plugin-related arguments. plugin_arg_suffix (str): The suffix to identify plugin-related arguments.
cli_args (Sequence[str]): A sequence of command-line arguments to parse. args (`Sequence[str]`): A sequence of command-line arguments to parse.
Returns: Returns:
dict: A dictionary containing the parsed plugin arguments where: dict: A dictionary containing the parsed plugin arguments where:
@@ -156,7 +157,7 @@ def load_plugin(plugin_path: str) -> None:
registered with their parents using the `register_subclass` decorator. registered with their parents using the `register_subclass` decorator.
Args: Args:
plugin_path (str): The Python package path to the plugin (e.g. "mypackage.plugins.myplugin") plugin_path (str): The Python package path to the plugin, e.g. "mypackage.plugins.myplugin".
Raises: Raises:
PluginLoadError: If the plugin cannot be loaded due to import errors or if the package path is invalid. PluginLoadError: If the plugin cannot be loaded due to import errors or if the package path is invalid.
@@ -180,6 +181,7 @@ def load_plugin(plugin_path: str) -> None:
) from e ) from e
def iter_namespace(ns_pkg: ModuleType) -> Iterable[ModuleInfo]: def iter_namespace(ns_pkg: ModuleType) -> Iterable[ModuleInfo]:
"""Iterate the direct submodules of `ns_pkg`, yielding their fully-qualified names."""
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".") return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")
try: try:
@@ -192,6 +194,7 @@ def load_plugin(plugin_path: str) -> None:
def get_path_arg(field_name: str, args: Sequence[str] | None = None) -> str | None: def get_path_arg(field_name: str, args: Sequence[str] | None = None) -> str | None:
"""Return `--{field_name}.path`'s value from CLI `args`, or from a YAML/JSON config if not on the CLI."""
result = parse_arg(f"{field_name}.{PATH_KEY}", args) result = parse_arg(f"{field_name}.{PATH_KEY}", args)
if result is None: if result is None:
result = _config_path_args.get(field_name) result = _config_path_args.get(field_name)
@@ -199,21 +202,24 @@ def get_path_arg(field_name: str, args: Sequence[str] | None = None) -> str | No
def get_yaml_overrides(field_name: str) -> list[str]: def get_yaml_overrides(field_name: str) -> list[str]:
"""Return the CLI-style overrides extracted from `field_name`'s YAML/JSON config path block, if any."""
return _config_yaml_overrides.get(field_name, []) return _config_yaml_overrides.get(field_name, [])
def get_type_arg(field_name: str, args: Sequence[str] | None = None) -> str | None: def get_type_arg(field_name: str, args: Sequence[str] | None = None) -> str | None:
"""Return `--{field_name}.type`'s value from CLI `args` (`sys.argv[1:]` if `None`)."""
return parse_arg(f"{field_name}.{draccus.CHOICE_TYPE_KEY}", args) return parse_arg(f"{field_name}.{draccus.CHOICE_TYPE_KEY}", args)
def _register_scoped_actions( def _register_scoped_actions(
wrapper: Wrapper, parser: SuppressingArgumentParser, cli_args: Sequence[str] wrapper: Wrapper, parser: SuppressingArgumentParser, cli_args: Sequence[str]
) -> None: ) -> None:
"""Like draccus's own Wrapper.register_actions, but for a ChoiceType field only recurses into """Like draccus's own Wrapper.register_actions, but only recurses a ChoiceType field's subclasses.
the already-selected subclass (per CLI `.type` args), instead of every registered choice.
This mirrors draccus 0.11.x's internal wrapper traversal because its public parser eagerly registers Only the already-selected subclass (per CLI `.type` args) is recursed into, instead of every
every choice before parsing the command line. Keep this in sync when updating draccus. registered choice. This mirrors draccus 0.11.x's internal wrapper traversal because its public
parser eagerly registers every choice before parsing the command line. Keep this in sync when
updating draccus.
""" """
if isinstance(wrapper, ChoiceWrapper): if isinstance(wrapper, ChoiceWrapper):
group = parser.add_argument_group(title=wrapper.title, description=wrapper.description) group = parser.add_argument_group(title=wrapper.title, description=wrapper.description)
@@ -253,8 +259,10 @@ def _register_scoped_actions(
def print_scoped_help(config_class: type, cli_args: Sequence[str]) -> None: def print_scoped_help(config_class: type, cli_args: Sequence[str]) -> None:
"""Prints --help output scoped to the choices already resolved on the CLI (e.g. --env.type=pusht), """Prints --help output scoped to the choices already resolved on the CLI (e.g. --env.type=pusht).
instead of draccus's default of expanding every registered subclass of every ChoiceType field."""
Instead of draccus's default of expanding every registered subclass of every ChoiceType field.
"""
parser = SuppressingArgumentParser(formatter_class=SimpleHelpFormatter) parser = SuppressingArgumentParser(formatter_class=SimpleHelpFormatter)
parser.add_argument( parser.add_argument(
f"--{draccus.utils.CONFIG_ARG}", type=str, help="Path for a config file to parse with draccus" f"--{draccus.utils.CONFIG_ARG}", type=str, help="Path for a config file to parse with draccus"
@@ -264,6 +272,7 @@ def print_scoped_help(config_class: type, cli_args: Sequence[str]) -> None:
def filter_arg(field_to_filter: str, args: Sequence[str] | None = None) -> list[str]: def filter_arg(field_to_filter: str, args: Sequence[str] | None = None) -> list[str]:
"""Return `args` with `--{field_to_filter}` (and its value, if separate) removed."""
if args is None: if args is None:
return [] return []
option = f"--{field_to_filter}" option = f"--{field_to_filter}"
@@ -285,12 +294,11 @@ def filter_arg(field_to_filter: str, args: Sequence[str] | None = None) -> list[
def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | None = None) -> list[str]: def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | None = None) -> list[str]:
""" """Filters command-line arguments related to fields with specific path arguments.
Filters command-line arguments related to fields with specific path arguments.
Args: Args:
fields_to_filter (str | list[str]): A single str or a list of str whose arguments need to be filtered. fields_to_filter (str | list[str]): A single str or a list of str whose arguments need to be filtered.
args (Sequence[str] | None): The sequence of command-line arguments to be filtered. args (Sequence[str] | None, *optional*): The sequence of command-line arguments to be filtered.
Defaults to None. Defaults to None.
Returns: Returns:
@@ -380,19 +388,22 @@ def extract_path_fields_from_config(config_path: str, path_fields: list[str]) ->
def wrap(config_path: Path | None = None) -> Callable[[F], F]: def wrap(config_path: Path | None = None) -> Callable[[F], F]:
""" """HACK: Similar to draccus.wrap but does three additional things.
HACK: Similar to draccus.wrap but does three additional things:
- Will remove '.path' arguments from CLI in order to process them later on. - Will remove '.path' arguments from CLI in order to process them later on.
- If a 'config_path' is passed and the main config class has a 'from_pretrained' method, will - If a 'config_path' is passed and the main config class has a 'from_pretrained' method, will
initialize it from there to allow to fetch configs from the hub directly initialize it from there to allow to fetch configs from the hub directly
- Will load plugins specified in the CLI arguments. These plugins will typically register - Will load plugins specified in the CLI arguments. These plugins will typically register
their own subclasses of config classes, so that draccus can find the right class to instantiate their own subclasses of config classes, so that draccus can find the right class to instantiate
from the CLI '.type' arguments from the CLI '.type' arguments
""" """
def wrapper_outer(fn: F) -> F: def wrapper_outer(fn: F) -> F:
"""Wrap `fn` so its first argument is resolved from the CLI/config instead of passed directly."""
@wraps(fn) @wraps(fn)
def wrapper_inner(*args: Any, **kwargs: Any) -> Any: def wrapper_inner(*args: Any, **kwargs: Any) -> Any:
"""Build `fn`'s config argument from the CLI/config file (unless already given), then call `fn`."""
argspec = inspect.getfullargspec(fn) argspec = inspect.getfullargspec(fn)
argtype = argspec.annotations[argspec.args[0]] argtype = argspec.annotations[argspec.args[0]]
if len(args) > 0 and type(args[0]) is argtype: if len(args) > 0 and type(args[0]) is argtype:
+88 -21
View File
@@ -39,50 +39,64 @@ logger = getLogger(__name__)
@dataclass @dataclass
class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: ignore[misc,name-defined] #TODO: draccus issue class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: ignore[misc,name-defined] #TODO: draccus issue
""" """Base configuration class for policy models.
Base configuration class for policy models.
Every concrete policy config also declares a `normalization_mapping: dict[str, NormalizationMode]`
field (mapping a `FeatureType` name, e.g. `"STATE"`/`"VISUAL"`, to the `NormalizationMode` to apply),
with a policy-specific default — not declared here since it has no sensible shared default.
Args: Args:
n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the n_obs_steps (`int`, *optional*, defaults to 1): Number of environment steps worth of observations
current step and additional steps going back). to pass to the policy (takes the current step and additional steps going back).
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents input_features (`dict[str, PolicyFeature] | None`, *optional*): A dictionary defining the
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes. `PolicyFeature` of the input data for the policy. The key represents the input data name, and
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents the value is a `PolicyFeature`, which consists of `type` and `shape` attributes. Can be set to
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes. `None`/`null` in order to infer those values from the dataset.
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to output_features (`dict[str, PolicyFeature] | None`, *optional*): A dictionary defining the
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX) `PolicyFeature` of the output data for the policy, with the same key/value semantics as
`input_features`.
device (`str | None`, *optional*): The torch device, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`.
If unset or unavailable, `__post_init__` auto-selects one.
use_amp (`bool`, *optional*, defaults to `False`): Whether to use Automatic Mixed Precision for
training and evaluation, with automatic gradient scaling. Auto-disabled by `__post_init__`
when AMP isn't available on `device`.
use_peft (`bool`, *optional*, defaults to `False`): Whether the policy employed PEFT for training.
push_to_hub (`bool`, *optional*, defaults to `True`): Whether to push the policy to the Hugging Face
Hub after training.
repo_id (`str | None`, *optional*): The Hub repo ID to push to. Required when `push_to_hub` is
`True`.
private (`bool | None`, *optional*): Whether to upload to a private repository on the Hugging Face
Hub.
tags (`list[str] | None`, *optional*): Tags to add to the policy on the Hub.
license (`str | None`, *optional*): The license to add to the policy on the Hub.
pretrained_path (`Path | None`, *optional*): Either the repo ID of a model hosted on the Hub or a
path to a directory containing weights saved using `PreTrainedPolicy.save_pretrained`. If not
provided, the policy is initialized from scratch.
pretrained_revision (`str | None`, *optional*): Hub revision (commit hash, branch, or tag) to pin
the pretrained model version.
""" """
n_obs_steps: int = 1 n_obs_steps: int = 1
# `input_features` can be set to None/null in order to infer those values from the dataset.
input_features: dict[str, PolicyFeature] | None = field(default_factory=dict) input_features: dict[str, PolicyFeature] | None = field(default_factory=dict)
output_features: dict[str, PolicyFeature] | None = field(default_factory=dict) output_features: dict[str, PolicyFeature] | None = field(default_factory=dict)
device: str | None = None # e.g. "cuda", "cuda:0", "cpu", or "mps" device: str | None = None
# `use_amp` determines whether to use Automatic Mixed Precision (AMP) for training and evaluation. With AMP,
# automatic gradient scaling is used.
use_amp: bool = False use_amp: bool = False
# Whether the policy employed PEFT for training.
use_peft: bool = False use_peft: bool = False
push_to_hub: bool = True # type: ignore[assignment] # TODO: use a different name to avoid override push_to_hub: bool = True # type: ignore[assignment] # TODO: use a different name to avoid override
repo_id: str | None = None repo_id: str | None = None
# Upload on private repository on the Hugging Face hub.
private: bool | None = None private: bool | None = None
# Add tags to your policy on the hub.
tags: list[str] | None = None tags: list[str] | None = None
# Add tags to your policy on the hub.
license: str | None = None license: str | None = None
# Either the repo ID of a model hosted on the Hub or a path to a directory containing weights
# saved using `Policy.save_pretrained`. If not provided, the policy is initialized from scratch.
pretrained_path: Path | None = None pretrained_path: Path | None = None
# Optional Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
pretrained_revision: str | None = None pretrained_revision: str | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Auto-select `device` when unset/unavailable, and disable `use_amp` when AMP isn't available on it."""
if not self.device or not is_torch_device_available(self.device): if not self.device or not is_torch_device_available(self.device):
auto_device = auto_select_torch_device() auto_device = auto_select_torch_device()
logger.warning(f"Device '{self.device}' is not available. Switching to '{auto_device}'.") logger.warning(f"Device '{self.device}' is not available. Switching to '{auto_device}'.")
@@ -97,6 +111,7 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
@property @property
def type(self) -> str: def type(self) -> str:
"""The policy's registered `draccus.ChoiceRegistry` name (e.g. `"act"`, `"diffusion"`)."""
choice_name = self.get_choice_name(self.__class__) choice_name = self.get_choice_name(self.__class__)
if not isinstance(choice_name, str): if not isinstance(choice_name, str):
raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}") raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}")
@@ -105,32 +120,52 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
@property @property
@abc.abstractmethod @abc.abstractmethod
def observation_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation def observation_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
"""Offsets, relative to the current step, of the observation timesteps the policy consumes.
`None` means only the current step is used.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def action_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation def action_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
"""Offsets, relative to the current step, of the action timesteps the policy predicts/consumes.
`None` means only the current step is used.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def reward_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation def reward_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
"""Offsets, relative to the current step, of the reward timesteps the policy consumes.
`None` means only the current step is used.
"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def get_optimizer_preset(self) -> OptimizerConfig: def get_optimizer_preset(self) -> OptimizerConfig:
"""Return this policy's default `OptimizerConfig`, used when `use_policy_training_preset` is set."""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def get_scheduler_preset(self) -> LRSchedulerConfig | None: def get_scheduler_preset(self) -> LRSchedulerConfig | None:
"""Return this policy's default `LRSchedulerConfig`, or `None` if it uses no scheduler."""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def validate_features(self) -> None: def validate_features(self) -> None:
"""Check that `input_features`/`output_features` contain what this policy requires.
Raises:
ValueError: If a required feature is missing or has an unexpected shape/type.
"""
raise NotImplementedError raise NotImplementedError
@property @property
def robot_state_feature(self) -> PolicyFeature | None: def robot_state_feature(self) -> PolicyFeature | None:
"""The input `PolicyFeature` for the robot's proprioceptive state (`observation.state`), if any."""
if not self.input_features: if not self.input_features:
return None return None
for ft_name, ft in self.input_features.items(): for ft_name, ft in self.input_features.items():
@@ -140,6 +175,7 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
@property @property
def env_state_feature(self) -> PolicyFeature | None: def env_state_feature(self) -> PolicyFeature | None:
"""The input `PolicyFeature` of type `FeatureType.ENV` (environment state), if any."""
if not self.input_features: if not self.input_features:
return None return None
for _, ft in self.input_features.items(): for _, ft in self.input_features.items():
@@ -149,12 +185,14 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
@property @property
def image_features(self) -> dict[str, PolicyFeature]: def image_features(self) -> dict[str, PolicyFeature]:
"""All input features of type `FeatureType.VISUAL`, keyed by feature name."""
if not self.input_features: if not self.input_features:
return {} return {}
return {key: ft for key, ft in self.input_features.items() if ft.type is FeatureType.VISUAL} return {key: ft for key, ft in self.input_features.items() if ft.type is FeatureType.VISUAL}
@property @property
def action_feature(self) -> PolicyFeature | None: def action_feature(self) -> PolicyFeature | None:
"""The output `PolicyFeature` for the action (`action`), if any."""
if not self.output_features: if not self.output_features:
return None return None
for ft_name, ft in self.output_features.items(): for ft_name, ft in self.output_features.items():
@@ -182,6 +220,35 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
revision: str | None = None, revision: str | None = None,
**policy_kwargs: Any, **policy_kwargs: Any,
) -> T: ) -> T:
"""Download a policy's `config.json` from the Hub (or read it locally) and parse it.
The concrete policy config subclass is resolved from the serialized `"type"` tag (e.g. `"act"`,
`"diffusion"`) rather than being fixed by `cls`, so calling this on the `PreTrainedConfig` base
class works for any registered policy type.
Args:
pretrained_name_or_path (`str | Path`): Either the `repo_id` of the config hosted on the Hub,
or a path to a directory containing a `config.json` saved via `.save_pretrained`.
force_download (`bool`, *optional*, defaults to `False`): Whether to force (re-)downloading
the files from the Hub, overriding the existing cache.
resume_download (`bool | None`, *optional*): Deprecated; ignored by the underlying Hub client.
proxies (`dict[Any, Any] | None`, *optional*): A dictionary of proxy servers to use by protocol
or endpoint.
token (`str | bool | None`, *optional*): The token to use as HTTP bearer authorization for
remote files. By default, uses the token cached by `huggingface-cli login`.
cache_dir (`str | Path | None`, *optional*): Path to the folder where cached files are stored.
local_files_only (`bool`, *optional*, defaults to `False`): If `True`, avoid downloading the
file and return the path to the local cached file if it exists.
revision (`str | None`, *optional*): Revision on the Hub: a branch name, git tag, or commit id.
Defaults to the latest commit on `main`.
policy_kwargs: Forwarded as CLI-style overrides via `policy_kwargs["cli_overrides"]`
(a list of `--key=value` strings applied on top of the loaded config); any other keys are
ignored.
Raises:
FileNotFoundError: If `config.json` isn't found locally or on the Hub.
ValueError: If `config.json` has no `"type"` field, or its value isn't a registered policy type.
"""
model_id = str(pretrained_name_or_path) model_id = str(pretrained_name_or_path)
config_file: str | None = None config_file: str | None = None
if Path(model_id).is_dir(): if Path(model_id).is_dir():
+57 -7
View File
@@ -43,31 +43,45 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
"""Base configuration for reward models. """Base configuration for reward models.
Args: Args:
input_features: A dictionary defining the PolicyFeature of the input data for the reward. The key represents input_features (`dict[str, PolicyFeature]`, *optional*): A dictionary defining the `PolicyFeature`
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes. of the input data for the reward. The key represents the input data name, and the value is a
output_features: A dictionary defining the PolicyFeature of the output data for the reward. The key represents `PolicyFeature`, which consists of `type` and `shape` attributes.
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes. output_features (`dict[str, PolicyFeature]`, *optional*): A dictionary defining the `PolicyFeature`
of the output data for the reward, with the same key/value semantics as `input_features`.
device (`str | None`, *optional*): The torch device, e.g. `"cuda"`, `"cuda:0"`, `"cpu"`, or `"mps"`.
If unset or unavailable, `__post_init__` auto-selects one.
pretrained_path (`str | None`, *optional*): Either the repo ID of a model hosted on the Hub or a
path to a directory containing weights saved using `.save_pretrained`. If not provided, the
reward model is initialized from scratch.
pretrained_revision (`str | None`, *optional*): Optional Hub revision, e.g. a commit hash, branch,
or tag, to pin the pretrained reward model version.
push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the reward model to the
Hugging Face Hub after training.
repo_id (`str | None`, *optional*): The Hub repo ID to push to. Required when `push_to_hub` is
`True`.
license (`str | None`, *optional*): The license to add to the reward model on the Hub.
tags (`list[str] | None`, *optional*): Tags to add to the reward model on the Hub.
private (`bool | None`, *optional*): Whether to upload to a private repository on the Hugging Face
Hub.
""" """
# Reuses PolicyFeature
input_features: dict[str, PolicyFeature] = field(default_factory=dict) input_features: dict[str, PolicyFeature] = field(default_factory=dict)
output_features: dict[str, PolicyFeature] = field(default_factory=dict) output_features: dict[str, PolicyFeature] = field(default_factory=dict)
device: str | None = None device: str | None = None
pretrained_path: str | None = None pretrained_path: str | None = None
# Optional Hub revision (commit hash, branch, or tag) to pin the pretrained reward model version.
pretrained_revision: str | None = None pretrained_revision: str | None = None
push_to_hub: bool = False push_to_hub: bool = False
repo_id: str | None = None repo_id: str | None = None
# Hub metadata
license: str | None = None license: str | None = None
tags: list[str] | None = None tags: list[str] | None = None
private: bool | None = None private: bool | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Auto-select `device` when unset or unavailable."""
if not self.device or not is_torch_device_available(self.device): if not self.device or not is_torch_device_available(self.device):
auto_device = auto_select_torch_device() auto_device = auto_select_torch_device()
logger.warning(f"Device '{self.device}' is not available. Switching to '{auto_device}'.") logger.warning(f"Device '{self.device}' is not available. Switching to '{auto_device}'.")
@@ -75,6 +89,7 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
@property @property
def type(self) -> str: def type(self) -> str:
"""The reward model's registered `draccus.ChoiceRegistry` name."""
choice_name = self.get_choice_name(self.__class__) choice_name = self.get_choice_name(self.__class__)
if not isinstance(choice_name, str): if not isinstance(choice_name, str):
raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}") raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}")
@@ -82,14 +97,17 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
@property @property
def observation_delta_indices(self) -> list | None: # type: ignore[type-arg] def observation_delta_indices(self) -> list | None: # type: ignore[type-arg]
"""`None`: reward models consume only the current observation timestep."""
return None return None
@property @property
def action_delta_indices(self) -> list | None: # type: ignore[type-arg] def action_delta_indices(self) -> list | None: # type: ignore[type-arg]
"""`None`: reward models consume only the current action timestep."""
return None return None
@property @property
def reward_delta_indices(self) -> list | None: # type: ignore[type-arg] def reward_delta_indices(self) -> list | None: # type: ignore[type-arg]
"""`None`: reward models consume only the current reward timestep."""
return None return None
def get_optimizer_preset(self) -> OptimizerConfig | None: def get_optimizer_preset(self) -> OptimizerConfig | None:
@@ -97,9 +115,14 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
return None return None
def get_scheduler_preset(self) -> LRSchedulerConfig | None: def get_scheduler_preset(self) -> LRSchedulerConfig | None:
"""Default LR scheduler for this reward model. `None` here; overridden by subclasses that need one."""
return None return None
def validate_features(self) -> None: def validate_features(self) -> None:
"""Check that `input_features`/`output_features` contain what this reward model requires.
No-op here; overridden by subclasses that have required features.
"""
pass pass
def _save_pretrained(self, save_directory: Path) -> None: def _save_pretrained(self, save_directory: Path) -> None:
@@ -122,6 +145,33 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
revision: str | None = None, revision: str | None = None,
**reward_kwargs: Any, **reward_kwargs: Any,
) -> T: ) -> T:
"""Download a reward model's `config.json` from the Hub (or read it locally) and parse it.
The concrete reward-model config subclass is resolved from the serialized `"type"` tag, so
calling this on the `RewardModelConfig` base class works for any registered reward-model type.
Args:
pretrained_name_or_path (`str | Path`): Either the `repo_id` of the config hosted on the Hub,
or a path to a directory containing a `config.json` saved via `.save_pretrained`.
force_download (`bool`, *optional*, defaults to `False`): Whether to force (re-)downloading
the files from the Hub, overriding the existing cache.
resume_download (`bool | None`, *optional*): Deprecated; ignored by the underlying Hub client.
proxies (`dict[Any, Any] | None`, *optional*): A dictionary of proxy servers to use by protocol
or endpoint.
token (`str | bool | None`, *optional*): The token to use as HTTP bearer authorization for
remote files. By default, uses the token cached by `huggingface-cli login`.
cache_dir (`str | Path | None`, *optional*): Path to the folder where cached files are stored.
local_files_only (`bool`, *optional*, defaults to `False`): If `True`, avoid downloading the
file and return the path to the local cached file if it exists.
revision (`str | None`, *optional*): Revision on the Hub: a branch name, git tag, or commit id.
Defaults to the latest commit on `main`.
reward_kwargs: Forwarded as CLI-style overrides via `reward_kwargs["cli_overrides"]`
(a list of `--key=value` strings applied on top of the loaded config); any other keys are
ignored.
Raises:
FileNotFoundError: If `config.json` isn't found locally or on the Hub.
"""
model_id = str(pretrained_name_or_path) model_id = str(pretrained_name_or_path)
config_file: str | None = None config_file: str | None = None
if Path(model_id).is_dir(): if Path(model_id).is_dir():
+121 -32
View File
@@ -108,75 +108,119 @@ def _migrate_legacy_rabc_fields(config: dict[str, Any]) -> dict[str, Any] | None
@dataclass @dataclass
class TrainPipelineConfig(HubMixin): class TrainPipelineConfig(HubMixin):
"""The top-level configuration for `lerobot-train`, parsed by draccus from CLI flags and/or a YAML file.
Args:
dataset (`DatasetConfig`): The dataset(s) to train on.
env (`envs.EnvConfig | None`, *optional*): The simulation environment to periodically evaluate the
policy in (see `env_eval_freq`). Required when `env_eval_freq > 0`.
policy (`PreTrainedConfig | None`, *optional*): The policy to train. Mutually exclusive with
`reward_model`.
reward_model (`RewardModelConfig | None`, *optional*): The reward model to train instead of a
policy. Mutually exclusive with `policy`.
output_dir (`Path | None`, *optional*): Where to save all of the run outputs. If you run another
training session with the same value its contents will be overwritten unless `resume` is set.
job_name (`str | None`, *optional*): A name for the run.
resume (`bool`, *optional*, defaults to `False`): Resume a previous run. Pass `--config_path`
pointing at either a local checkpoint's `train_config.json` or a Hub repo id holding
`checkpoints/<step>/` subtrees (the latest checkpoint is downloaded and resumed from). When
resuming, the default behavior is to use the configuration from the checkpoint, regardless of
what's provided with the training command at the time of resumption (CLI `--*` flags still
override).
seed (`int | None`, *optional*, defaults to 1000): Seed used for training (e.g. model
initialization, dataset shuffling) and for the evaluation environments.
cudnn_deterministic (`bool`, *optional*, defaults to `False`): Use deterministic cuDNN algorithms
for reproducibility. Disables `cudnn.benchmark` and may reduce training speed by ~10-20 percent.
num_workers (`int`, *optional*, defaults to 4): Number of workers for the dataloader.
batch_size (`int`, *optional*, defaults to 8): The training batch size.
prefetch_factor (`int`, *optional*, defaults to 4): Number of batches loaded in advance by each
dataloader worker.
persistent_workers (`bool`, *optional*, defaults to `True`): Keep dataloader worker processes alive
between epochs.
dataloader_multiprocessing_context (`str | None`, *optional*, defaults to `"spawn"`): DataLoader
worker start method. `"spawn"` is safer than `"fork"` with non-fork-safe libs (PyAV /
torchcodec / ffmpeg), but adds some worker-startup time per run since workers re-import modules
instead of inheriting parent state. Override with `--dataloader_multiprocessing_context=fork`
when appropriate, or set it to `None` to use Python's platform default.
steps (`int`, *optional*, defaults to 100000): Total number of training steps.
env_eval_freq (`int`, *optional*, defaults to 20000): Run the policy in the simulation environment
every N steps to measure reward/success (0 = disabled).
log_freq (`int`, *optional*, defaults to 200): Logging frequency, in steps.
eval_steps (`int`, *optional*, defaults to 0): Compute eval loss on held-out episodes every N steps
(0 = disabled). Requires `eval_split > 0`.
max_eval_samples (`int`, *optional*, defaults to 0): Cap on total eval samples, split uniformly
across tasks (0 = use all held-out data).
tolerance_s (`float`, *optional*, defaults to 0.0001): Maximum timestamp difference tolerated when
loading dataset frames, in seconds.
save_checkpoint (`bool`, *optional*, defaults to `True`): Whether to save checkpoints during
training.
save_freq (`int`, *optional*, defaults to 20000): Save a checkpoint every `save_freq` training
iterations and after the last training step. A non-positive value disables periodic saving,
keeping only the final checkpoint.
checkpoint_format (`CheckpointFormat`, *optional*, defaults to `CheckpointFormat.SAFETENSORS`):
Model-artifact format inside checkpoints; non-default values require a sharded run.
use_policy_training_preset (`bool`, *optional*, defaults to `True`): Use the policy's own
optimizer/scheduler presets when `optimizer`/`scheduler` aren't explicitly set.
optimizer (`OptimizerConfig | None`, *optional*): The optimizer to use. Falls back to the policy's
preset when `use_policy_training_preset` is `True`.
scheduler (`LRSchedulerConfig | None`, *optional*): The learning-rate scheduler to use. Falls back
to the policy's preset when `use_policy_training_preset` is `True`.
parallelism (`ParallelismConfig`, *optional*): Process topology: `dp_replicate` / `dp_shard` for HSDP
and context-parallel degree placeholders.
accelerator (`AcceleratorConfig`, *optional*): Execution runtime handed to the Accelerator: mixed
precision, gradient accumulation, FSDP/DDP tuning knobs, compile & activation-checkpointing
placeholders.
eval (`EvalConfig`, *optional*): Settings for the periodic simulation-environment evaluation.
wandb (`WandBConfig`, *optional*): Weights & Biases logging settings.
peft (`PeftConfig | None`, *optional*): PEFT (e.g. LoRA) settings, when fine-tuning with adapters
instead of full-parameter training.
job (`JobConfig`, *optional*): Where to run training: locally (default), or an HF Jobs flavor.
save_checkpoint_to_hub (`bool`, *optional*, defaults to `False`): Push each saved checkpoint to the
Hub (`policy.repo_id`) as it is written, not just the final model (useful to monitor progress
mid-run). The final model is pushed regardless. Works the same locally and remotely.
sample_weighting (`SampleWeightingConfig | None`, *optional*): Sample weighting configuration (e.g.
for RA-BC training).
rename_map (`dict[str, str]`, *optional*): Rename map for the observation, to override the image
and state keys.
"""
dataset: DatasetConfig dataset: DatasetConfig
env: envs.EnvConfig | None = None env: envs.EnvConfig | None = None
policy: PreTrainedConfig | None = None policy: PreTrainedConfig | None = None
reward_model: RewardModelConfig | None = None reward_model: RewardModelConfig | None = None
# Set `dir` to where you would like to save all of the run outputs. If you run another training session
# with the same value for `dir` its contents will be overwritten unless you set `resume` to true.
output_dir: Path | None = None output_dir: Path | None = None
job_name: str | None = None job_name: str | None = None
# Set `resume` to true to resume a previous run. Pass `--config_path` pointing at either a local
# checkpoint's train_config.json or a Hub repo id holding `checkpoints/<step>/` subtrees (the
# latest checkpoint is downloaded and resumed from). Note that when resuming, the default behavior
# is to use the configuration from the checkpoint, regardless of what's provided with the training
# command at the time of resumption (CLI `--*` flags still override).
resume: bool = False resume: bool = False
# `seed` is used for training (eg: model initialization, dataset shuffling)
# AND for the evaluation environments.
seed: int | None = 1000 seed: int | None = 1000
# Set to True to use deterministic cuDNN algorithms for reproducibility.
# This disables cudnn.benchmark and may reduce training speed by ~10-20 percent.
cudnn_deterministic: bool = False cudnn_deterministic: bool = False
# Number of workers for the dataloader.
num_workers: int = 4 num_workers: int = 4
batch_size: int = 8 batch_size: int = 8
prefetch_factor: int = 4 prefetch_factor: int = 4
persistent_workers: bool = True persistent_workers: bool = True
# DataLoader worker start method. "spawn" is safer than "fork" with
# non-fork-safe libs (PyAV / torchcodec / ffmpeg), but adds some
# worker-startup time per run since workers re-import modules instead
# of inheriting parent state. Override with `--dataloader_multiprocessing_context=fork`
# when appropriate, or set it to `null` to use Python's platform default.
dataloader_multiprocessing_context: str | None = "spawn" dataloader_multiprocessing_context: str | None = "spawn"
steps: int = 100_000 steps: int = 100_000
# Run policy in the simulation environment every N steps to measure reward/success (0 = disabled).
env_eval_freq: int = 20_000 env_eval_freq: int = 20_000
log_freq: int = 200 log_freq: int = 200
# Compute eval loss on held-out episodes every N steps (0 = disabled). Requires eval_split > 0.
eval_steps: int = 0 eval_steps: int = 0
# Cap on total eval samples, split uniformly across tasks (0 = use all held-out data).
max_eval_samples: int = 0 max_eval_samples: int = 0
tolerance_s: float = 1e-4 tolerance_s: float = 1e-4
save_checkpoint: bool = True save_checkpoint: bool = True
# Checkpoint is saved every `save_freq` training iterations and after the last training step.
# A non-positive value disables periodic saving, keeping only the final checkpoint.
save_freq: int = 20_000 save_freq: int = 20_000
# Model-artifact format inside checkpoints; non-default values require a sharded run.
checkpoint_format: CheckpointFormat = CheckpointFormat.SAFETENSORS checkpoint_format: CheckpointFormat = CheckpointFormat.SAFETENSORS
use_policy_training_preset: bool = True use_policy_training_preset: bool = True
optimizer: OptimizerConfig | None = None optimizer: OptimizerConfig | None = None
scheduler: LRSchedulerConfig | None = None scheduler: LRSchedulerConfig | None = None
# Process topology: dp_replicate / dp_shard (HSDP) and context-parallel degree placeholders.
parallelism: ParallelismConfig = field(default_factory=ParallelismConfig) parallelism: ParallelismConfig = field(default_factory=ParallelismConfig)
# Execution runtime handed to the Accelerator: mixed precision, gradient accumulation,
# FSDP/DDP tuning knobs, compile & activation-checkpointing placeholders.
accelerator: AcceleratorConfig = field(default_factory=AcceleratorConfig) accelerator: AcceleratorConfig = field(default_factory=AcceleratorConfig)
eval: EvalConfig = field(default_factory=EvalConfig) eval: EvalConfig = field(default_factory=EvalConfig)
wandb: WandBConfig = field(default_factory=WandBConfig) wandb: WandBConfig = field(default_factory=WandBConfig)
peft: PeftConfig | None = None peft: PeftConfig | None = None
# Where to run training (local default, or an HF Jobs flavor). See JobConfig.
job: JobConfig = field(default_factory=JobConfig) job: JobConfig = field(default_factory=JobConfig)
# Push each saved checkpoint to the Hub (policy.repo_id) as it is written, not
# just the final model (useful to monitor progress mid-run). Optional; the
# final model is pushed regardless. Works the same locally and remotely.
save_checkpoint_to_hub: bool = False save_checkpoint_to_hub: bool = False
# Sample weighting configuration (e.g., for RA-BC training)
sample_weighting: SampleWeightingConfig | None = None sample_weighting: SampleWeightingConfig | None = None
# Rename map for the observation to override the image and state keys
rename_map: dict[str, str] = field(default_factory=dict) rename_map: dict[str, str] = field(default_factory=dict)
checkpoint_path: Path | None = field(init=False, default=None) checkpoint_path: Path | None = field(init=False, default=None)
@@ -262,6 +306,21 @@ class TrainPipelineConfig(HubMixin):
self.reward_model.pretrained_path = str(policy_dir) self.reward_model.pretrained_path = str(policy_dir)
def validate(self) -> None: def validate(self) -> None:
"""Resolve pretrained sources and cross-field defaults, and fail fast on invalid combinations.
Called by draccus after parsing. Resolves `--policy.path`/`--reward_model.path`/`resume` into a
loaded config, derives `job_name` and `output_dir` when unset, and applies the policy's
optimizer/scheduler presets when `use_policy_training_preset` is `True`.
Raises:
ValueError: On an unsupported `dataloader_multiprocessing_context`, neither `policy` nor
`reward_model` configured, a `rename_map` without a pretrained checkpoint, an unsplit
dataset with `eval_steps > 0`, a missing `repo_id` when pushing to the Hub, or
`save_checkpoint_to_hub` without `policy.repo_id` — or (see `_validate_distributed`) an
unsupported distributed-training combination.
FileExistsError: If `output_dir` already exists and `resume` is `False`.
NotImplementedError: If `dataset.repo_id` is a list (multi-dataset training).
"""
available_contexts = multiprocessing.get_all_start_methods() available_contexts = multiprocessing.get_all_start_methods()
if ( if (
self.dataloader_multiprocessing_context is not None self.dataloader_multiprocessing_context is not None
@@ -389,6 +448,7 @@ class TrainPipelineConfig(HubMixin):
return ["policy", "reward_model"] return ["policy", "reward_model"]
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
"""Encode the config to a plain, JSON-serializable dictionary (via `draccus.encode`)."""
return draccus.encode(self) # type: ignore[no-any-return] # because of the third-party library draccus uses Any as the return type return draccus.encode(self) # type: ignore[no-any-return] # because of the third-party library draccus uses Any as the return type
def _save_pretrained(self, save_directory: Path) -> None: def _save_pretrained(self, save_directory: Path) -> None:
@@ -409,6 +469,35 @@ class TrainPipelineConfig(HubMixin):
revision: str | None = None, revision: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> "TrainPipelineConfig": ) -> "TrainPipelineConfig":
"""Download a run's `train_config.json` from the Hub (or read it locally) and parse it.
Falls back to the latest checkpoint's config when the repo has no root `train_config.json` (a repo
of periodic checkpoints from an interrupted run), so a resume can start straight from
`--config_path=<repo>`. Legacy RA-BC fields in a JSON config are migrated to the current
`sample_weighting` schema.
Args:
pretrained_name_or_path (`str | Path`): Either the `repo_id` of the run hosted on the Hub, or
a path to a directory containing a `train_config.json` saved via `.save_pretrained`.
force_download (`bool`, *optional*, defaults to `False`): Whether to force (re-)downloading
the files from the Hub, overriding the existing cache.
resume_download (`bool | None`, *optional*): Deprecated; ignored by the underlying Hub client.
proxies (`dict[Any, Any] | None`, *optional*): A dictionary of proxy servers to use by protocol
or endpoint.
token (`str | bool | None`, *optional*): The token to use as HTTP bearer authorization for
remote files. By default, uses the token cached by `huggingface-cli login`.
cache_dir (`str | Path | None`, *optional*): Path to the folder where cached files are stored.
local_files_only (`bool`, *optional*, defaults to `False`): If `True`, avoid downloading the
file and return the path to the local cached file if it exists.
revision (`str | None`, *optional*): Revision on the Hub: a branch name, git tag, or commit id.
Defaults to the latest commit on `main`.
kwargs: Forwarded as CLI-style overrides via `kwargs["cli_args"]` (a list of `--key=value`
strings applied on top of the loaded config); any other keys are ignored.
Raises:
FileNotFoundError: If `train_config.json` isn't found locally, on the Hub, or on any checkpoint
within the Hub repo.
"""
model_id = str(pretrained_name_or_path) model_id = str(pretrained_name_or_path)
config_file: str | None = None config_file: str | None = None
if Path(model_id).is_dir(): if Path(model_id).is_dir():
+48
View File
@@ -18,6 +18,18 @@ from enum import Enum
class FeatureType(str, Enum): class FeatureType(str, Enum):
"""The category of data a `PolicyFeature` represents.
**Attributes**:
- **STATE** -- A robot/environment proprioceptive state vector.
- **VISUAL** -- An image or video feature.
- **ENV** -- Environment-provided state, distinct from robot proprioception (e.g. simulation
environment state).
- **ACTION** -- An action vector.
- **REWARD** -- A scalar reward.
- **LANGUAGE** -- A natural-language feature (e.g. task instruction tokens).
"""
STATE = "STATE" STATE = "STATE"
VISUAL = "VISUAL" VISUAL = "VISUAL"
ENV = "ENV" ENV = "ENV"
@@ -27,11 +39,28 @@ class FeatureType(str, Enum):
class PipelineFeatureType(str, Enum): class PipelineFeatureType(str, Enum):
"""Which side of a processor pipeline a feature belongs to.
**Attributes**:
- **ACTION** -- The feature is part of the action space.
- **OBSERVATION** -- The feature is part of the observation space.
"""
ACTION = "ACTION" ACTION = "ACTION"
OBSERVATION = "OBSERVATION" OBSERVATION = "OBSERVATION"
class NormalizationMode(str, Enum): class NormalizationMode(str, Enum):
"""The normalization strategy applied to a feature by a `NormalizerProcessorStep`.
**Attributes**:
- **MIN_MAX** -- Scale to `[-1, 1]` using the feature's min/max statistics.
- **MEAN_STD** -- Center and scale to unit variance using the feature's mean/std statistics.
- **IDENTITY** -- Leave the feature unchanged.
- **QUANTILES** -- Scale to `[-1, 1]` using the feature's 1st/99th percentile statistics.
- **QUANTILE10** -- Scale to `[-1, 1]` using the feature's 10th/90th percentile statistics.
"""
MIN_MAX = "MIN_MAX" MIN_MAX = "MIN_MAX"
MEAN_STD = "MEAN_STD" MEAN_STD = "MEAN_STD"
IDENTITY = "IDENTITY" IDENTITY = "IDENTITY"
@@ -41,11 +70,30 @@ class NormalizationMode(str, Enum):
@dataclass @dataclass
class PolicyFeature: class PolicyFeature:
"""Describes one entry of a policy's input/output feature space.
Args:
type (`FeatureType`): The category of the feature.
shape (`tuple[int, ...]`): The feature's shape, excluding the batch dimension.
"""
type: FeatureType type: FeatureType
shape: tuple[int, ...] shape: tuple[int, ...]
class RTCAttentionSchedule(str, Enum): class RTCAttentionSchedule(str, Enum):
"""The prefix-attention weighting schedule used by the Real-Time Chunking (RTC) policy.
Controls how much weight is given to the previous action chunk's prediction versus the new one,
over the overlap region between consecutive chunks.
**Attributes**:
- **ZEROS** -- No prefix attention: weight is 1.0 before `start`, then 0.0.
- **ONES** -- Full prefix attention: weight is 1.0 up to `end`, then 0.0.
- **LINEAR** -- Linearly ramps the weight down from 1.0 to 0.0 between `start` and `end`.
- **EXP** -- Like `LINEAR`, but with an exponential (rather than linear) decay curve.
"""
ZEROS = "ZEROS" ZEROS = "ZEROS"
ONES = "ONES" ONES = "ONES"
LINEAR = "LINEAR" LINEAR = "LINEAR"
+34 -17
View File
@@ -84,18 +84,33 @@ DEPTH_ENCODER_INFO_FIELD_NAMES: frozenset[str] = frozenset({"depth_min", "depth_
@dataclass @dataclass
class VideoEncoderConfig: class VideoEncoderConfig:
"""Video encoder configuration.""" """Video encoder configuration.
vcodec: str = "libsvtav1" # Video codec name. "auto" picks a hardware codec if available, else libsvtav1. Args:
pix_fmt: str = "yuv420p" # Pixel format (e.g. yuv420p). vcodec (`str`, *optional*, defaults to `"libsvtav1"`): Video codec name. `"auto"` picks a hardware
g: int | None = 2 # GOP size (keyframe interval). codec if available, else `libsvtav1`.
crf: int | float | None = 30 # Quality level. Lower means better quality and larger files. pix_fmt (`str`, *optional*, defaults to `"yuv420p"`): Pixel format (e.g. `yuv420p`).
preset: int | str | None = None # Speed/quality preset. Accepted values are codec-specific. g (`int | None`, *optional*, defaults to 2): GOP size (keyframe interval).
fast_decode: int = 0 # Fast-decode tuning. Accepted values are codec-specific, 0 disables it. crf (`int | float | None`, *optional*, defaults to 30): Quality level. Lower means better quality
and larger files.
preset (`int | str | None`, *optional*): Speed/quality preset. Accepted values are codec-specific.
fast_decode (`int`, *optional*, defaults to 0): Fast-decode tuning. Accepted values are
codec-specific; 0 disables it.
video_backend (`str`, *optional*, defaults to `"pyav"`): Encoding backend. Only `"pyav"` is
currently supported.
extra_options (`dict[str, Any]`, *optional*): Extra codec options merged last, e.g. `{"tune":
"film"}`.
"""
vcodec: str = "libsvtav1"
pix_fmt: str = "yuv420p"
g: int | None = 2
crf: int | float | None = 30
preset: int | str | None = None
fast_decode: int = 0
# TODO(CarolinePascal): add torchcodec support + find a way to unify the # TODO(CarolinePascal): add torchcodec support + find a way to unify the
# two backends (encoding and decoding). # two backends (encoding and decoding).
video_backend: str = "pyav" # Encoding backend. Only "pyav" is currently supported. video_backend: str = "pyav"
# Extra codec options merged last, e.g. {"tune": "film"}.
extra_options: dict[str, Any] = field(default_factory=dict) extra_options: dict[str, Any] = field(default_factory=dict)
# Source-data channel count this encoder is expected to handle. ``None`` # Source-data channel count this encoder is expected to handle. ``None``
@@ -104,6 +119,7 @@ class VideoEncoderConfig:
_DEFAULT_CHANNELS: ClassVar[int | None] = None _DEFAULT_CHANNELS: ClassVar[int | None] = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Resolve `vcodec` (e.g. `"auto"`), apply the libsvtav1 default preset, and validate the config."""
self.resolve_vcodec() self.resolve_vcodec()
# Empty-constructor ergonomics: ``VideoEncoderConfig()`` must "just work". # Empty-constructor ergonomics: ``VideoEncoderConfig()`` must "just work".
if self.preset is None and self.vcodec == "libsvtav1": if self.preset is None and self.vcodec == "libsvtav1":
@@ -112,9 +128,7 @@ class VideoEncoderConfig:
@classmethod @classmethod
def _kwargs_from_video_info(cls, video_info: dict | None) -> dict[str, Any]: def _kwargs_from_video_info(cls, video_info: dict | None) -> dict[str, Any]:
"""Parse the ``video.*`` keys of a feature ``info`` block into """Parse the ``video.*`` keys of a feature ``info`` block into constructor kwargs."""
constructor kwargs.
"""
video_info = video_info or {} video_info = video_info or {}
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {}
@@ -147,6 +161,7 @@ class VideoEncoderConfig:
Args: Args:
encoders: List of encoder names to detect. If a string, it is converted to a list. encoders: List of encoder names to detect. If a string, it is converted to a list.
Returns: Returns:
List of available encoder names. If the video backend is not "pyav", returns an empty list. List of available encoder names. If the video backend is not "pyav", returns an empty list.
""" """
@@ -211,6 +226,7 @@ class VideoEncoderConfig:
opts: dict[str, Any] = {} opts: dict[str, Any] = {}
def set_if(key: str, value: Any) -> None: def set_if(key: str, value: Any) -> None:
"""Set `opts[key]` to `value` (stringified if `as_strings`), unless `value` is `None`."""
if value is not None: if value is not None:
opts[key] = value if not as_strings else str(value) opts[key] = value if not as_strings else str(value)
@@ -302,9 +318,10 @@ class DepthEncoderConfig(VideoEncoderConfig):
@classmethod @classmethod
def _kwargs_from_video_info(cls, video_info: dict | None) -> dict[str, Any]: def _kwargs_from_video_info(cls, video_info: dict | None) -> dict[str, Any]:
"""Layer the depth-specific tuning (``depth_min`` / ``depth_max`` / """Layer the depth-specific tuning on top of the base parser.
``shift`` / ``use_log``) on top of the base parser. Missing keys
fall back to the class defaults. Adds ``depth_min`` / ``depth_max`` / ``shift`` / ``use_log``. Missing keys fall back to the
class defaults.
""" """
kwargs = super()._kwargs_from_video_info(video_info) kwargs = super()._kwargs_from_video_info(video_info)
video_info = video_info or {} video_info = video_info or {}
@@ -328,8 +345,8 @@ def encoder_config_from_video_info(video_info: dict | None) -> VideoEncoderConfi
otherwise. otherwise.
Args: Args:
video_info: A feature's ``info`` dict as persisted in ``info.json``, video_info (`dict | None`): A feature's ``info`` dict as persisted in ``info.json``, or ``None``
or ``None`` (treated as an empty dict). (treated as an empty dict).
Returns: Returns:
A :class:`DepthEncoderConfig` for depth features, otherwise a A :class:`DepthEncoderConfig` for depth features, otherwise a
+4 -10
View File
@@ -25,20 +25,14 @@ from lerobot.policies import PreTrainedPolicy
def make_optimizer_and_scheduler( def make_optimizer_and_scheduler(
cfg: TrainPipelineConfig, policy: PreTrainedPolicy cfg: TrainPipelineConfig, policy: PreTrainedPolicy
) -> tuple[Optimizer, LRScheduler | None]: ) -> tuple[Optimizer, LRScheduler | None]:
"""Build the optimizer and, if configured, the learning rate scheduler for training a policy. """Generates the optimizer and scheduler based on configs.
Args: Args:
cfg (`TrainPipelineConfig`): cfg (TrainPipelineConfig): The training config that contains optimizer and scheduler configs
The training config, whose `optimizer` and `scheduler` fields are built. policy (PreTrainedPolicy): The policy config from which parameters and presets must be taken from.
policy (`PreTrainedPolicy`):
The policy being trained; its parameters (or optimizer-preset groups, if
`cfg.use_policy_training_preset` is `True`) are passed to the optimizer.
Returns: Returns:
`tuple[Optimizer, LRScheduler | None]`: The built optimizer, and scheduler if one was configured. tuple[Optimizer, LRScheduler | None]: The couple (Optimizer, Scheduler). Scheduler can be `None`.
Raises:
ValueError: If `cfg.optimizer` is `None`.
""" """
params = policy.get_optim_params() if cfg.use_policy_training_preset else policy.parameters() params = policy.get_optim_params() if cfg.use_policy_training_preset else policy.parameters()
if cfg.optimizer is None: if cfg.optimizer is None:
+13 -120
View File
@@ -44,32 +44,12 @@ OptimizerParams = (
@dataclass @dataclass
class OptimizerConfig(draccus.ChoiceRegistry, abc.ABC): class OptimizerConfig(draccus.ChoiceRegistry, abc.ABC):
"""Base configuration shared by every optimizer.
Concrete optimizers subclass this and register themselves with
`@OptimizerConfig.register_subclass("name")`, which is what makes `--optimizer.type=name` work on the
command line.
Args:
lr (`float`):
Learning rate.
weight_decay (`float`):
Weight decay (L2 penalty) applied by the optimizer.
grad_clip_norm (`float`):
Maximum gradient norm; gradients are clipped to this value before each optimizer step.
"""
lr: float lr: float
weight_decay: float weight_decay: float
grad_clip_norm: float grad_clip_norm: float
@property @property
def type(self) -> str: def type(self) -> str:
"""Return the registered name this config was registered under.
Returns:
`str`: The name passed to `@OptimizerConfig.register_subclass`, e.g. `"adam"`.
"""
return self.get_choice_name(self.__class__) return self.get_choice_name(self.__class__)
@property @property
@@ -79,16 +59,12 @@ class OptimizerConfig(draccus.ChoiceRegistry, abc.ABC):
@classmethod @classmethod
def default_choice_name(cls) -> str | None: def default_choice_name(cls) -> str | None:
"""Return the registered name used when `--optimizer.type` is not specified.
Returns:
`str | None`: `"adam"`.
"""
return "adam" return "adam"
@abc.abstractmethod @abc.abstractmethod
def build(self, params: OptimizerParams) -> torch.optim.Optimizer | dict[str, torch.optim.Optimizer]: def build(self, params: OptimizerParams) -> torch.optim.Optimizer | dict[str, torch.optim.Optimizer]:
"""Build the optimizer. It can be a single optimizer or a dictionary of optimizers. """
Build the optimizer. It can be a single optimizer or a dictionary of optimizers.
NOTE: Multiple optimizers are useful when you have different models to optimize. NOTE: Multiple optimizers are useful when you have different models to optimize.
For example, you can have one optimizer for the policy and another one for the value function For example, you can have one optimizer for the policy and another one for the value function
@@ -113,21 +89,6 @@ class OptimizerConfig(draccus.ChoiceRegistry, abc.ABC):
@OptimizerConfig.register_subclass("adam") @OptimizerConfig.register_subclass("adam")
@dataclass @dataclass
class AdamConfig(OptimizerConfig): class AdamConfig(OptimizerConfig):
"""Configuration for [`torch.optim.Adam`](https://docs.pytorch.org/docs/stable/generated/torch.optim.Adam.html).
Args:
lr (`float`, *optional*, defaults to 0.001):
Learning rate.
weight_decay (`float`, *optional*, defaults to 0.0):
Weight decay (L2 penalty).
grad_clip_norm (`float`, *optional*, defaults to 10.0):
Maximum gradient norm; gradients are clipped to this value before each optimizer step.
betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.999)`):
Coefficients used for computing running averages of the gradient and its square.
eps (`float`, *optional*, defaults to 1e-08):
Term added to the denominator to improve numerical stability.
"""
lr: float = 1e-3 lr: float = 1e-3
betas: tuple[float, float] = (0.9, 0.999) betas: tuple[float, float] = (0.9, 0.999)
eps: float = 1e-8 eps: float = 1e-8
@@ -135,15 +96,6 @@ class AdamConfig(OptimizerConfig):
grad_clip_norm: float = 10.0 grad_clip_norm: float = 10.0
def build(self, params: OptimizerParams) -> torch.optim.Optimizer: def build(self, params: OptimizerParams) -> torch.optim.Optimizer:
"""Build a [`torch.optim.Adam`](https://docs.pytorch.org/docs/stable/generated/torch.optim.Adam.html) instance from this config.
Args:
params (`OptimizerParams`):
Parameters to optimize, as accepted by `torch.optim.Adam`.
Returns:
`torch.optim.Optimizer`: The built optimizer.
"""
kwargs = asdict(self) kwargs = asdict(self)
kwargs.pop("grad_clip_norm") kwargs.pop("grad_clip_norm")
return torch.optim.Adam(params, **kwargs) return torch.optim.Adam(params, **kwargs)
@@ -152,21 +104,6 @@ class AdamConfig(OptimizerConfig):
@OptimizerConfig.register_subclass("adamw") @OptimizerConfig.register_subclass("adamw")
@dataclass @dataclass
class AdamWConfig(OptimizerConfig): class AdamWConfig(OptimizerConfig):
"""Configuration for [`torch.optim.AdamW`](https://docs.pytorch.org/docs/stable/generated/torch.optim.AdamW.html).
Args:
lr (`float`, *optional*, defaults to 0.001):
Learning rate.
weight_decay (`float`, *optional*, defaults to 0.01):
Weight decay, applied decoupled from the gradient update as in the AdamW paper.
grad_clip_norm (`float`, *optional*, defaults to 10.0):
Maximum gradient norm; gradients are clipped to this value before each optimizer step.
betas (`tuple[float, float]`, *optional*, defaults to `(0.9, 0.999)`):
Coefficients used for computing running averages of the gradient and its square.
eps (`float`, *optional*, defaults to 1e-08):
Term added to the denominator to improve numerical stability.
"""
lr: float = 1e-3 lr: float = 1e-3
betas: tuple[float, float] = (0.9, 0.999) betas: tuple[float, float] = (0.9, 0.999)
eps: float = 1e-8 eps: float = 1e-8
@@ -174,15 +111,6 @@ class AdamWConfig(OptimizerConfig):
grad_clip_norm: float = 10.0 grad_clip_norm: float = 10.0
def build(self, params: OptimizerParams) -> torch.optim.Optimizer: def build(self, params: OptimizerParams) -> torch.optim.Optimizer:
"""Build a [`torch.optim.AdamW`](https://docs.pytorch.org/docs/stable/generated/torch.optim.AdamW.html) instance from this config.
Args:
params (`OptimizerParams`):
Parameters to optimize, as accepted by `torch.optim.AdamW`.
Returns:
`torch.optim.Optimizer`: The built optimizer.
"""
kwargs = asdict(self) kwargs = asdict(self)
kwargs.pop("grad_clip_norm") kwargs.pop("grad_clip_norm")
return torch.optim.AdamW(params, **kwargs) return torch.optim.AdamW(params, **kwargs)
@@ -191,23 +119,6 @@ class AdamWConfig(OptimizerConfig):
@OptimizerConfig.register_subclass("sgd") @OptimizerConfig.register_subclass("sgd")
@dataclass @dataclass
class SGDConfig(OptimizerConfig): class SGDConfig(OptimizerConfig):
"""Configuration for [`torch.optim.SGD`](https://docs.pytorch.org/docs/stable/generated/torch.optim.SGD.html).
Args:
lr (`float`, *optional*, defaults to 0.001):
Learning rate.
weight_decay (`float`, *optional*, defaults to 0.0):
Weight decay (L2 penalty).
grad_clip_norm (`float`, *optional*, defaults to 10.0):
Maximum gradient norm; gradients are clipped to this value before each optimizer step.
momentum (`float`, *optional*, defaults to 0.0):
Momentum factor.
dampening (`float`, *optional*, defaults to 0.0):
Dampening for momentum.
nesterov (`bool`, *optional*, defaults to `False`):
Whether to enable Nesterov momentum.
"""
lr: float = 1e-3 lr: float = 1e-3
momentum: float = 0.0 momentum: float = 0.0
dampening: float = 0.0 dampening: float = 0.0
@@ -216,15 +127,6 @@ class SGDConfig(OptimizerConfig):
grad_clip_norm: float = 10.0 grad_clip_norm: float = 10.0
def build(self, params: OptimizerParams) -> torch.optim.Optimizer: def build(self, params: OptimizerParams) -> torch.optim.Optimizer:
"""Build a [`torch.optim.SGD`](https://docs.pytorch.org/docs/stable/generated/torch.optim.SGD.html) instance from this config.
Args:
params (`OptimizerParams`):
Parameters to optimize, as accepted by `torch.optim.SGD`.
Returns:
`torch.optim.Optimizer`: The built optimizer.
"""
kwargs = asdict(self) kwargs = asdict(self)
kwargs.pop("grad_clip_norm") kwargs.pop("grad_clip_norm")
return torch.optim.SGD(params, **kwargs) return torch.optim.SGD(params, **kwargs)
@@ -266,7 +168,8 @@ class XVLAAdamWConfig(OptimizerConfig):
soft_prompt_warmup_lr_scale: float | None = None # If set, start soft-prompts at this scale (e.g., 0.01) soft_prompt_warmup_lr_scale: float | None = None # If set, start soft-prompts at this scale (e.g., 0.01)
def build(self, params: OptimizerParams) -> torch.optim.Optimizer: def build(self, params: OptimizerParams) -> torch.optim.Optimizer:
"""Build AdamW optimizer with differential learning rates. """
Build AdamW optimizer with differential learning rates.
Args: Args:
params: Must be a dict[str, Parameter] from dict(model.named_parameters()) params: Must be a dict[str, Parameter] from dict(model.named_parameters())
@@ -336,14 +239,10 @@ class MultiAdamConfig(OptimizerConfig):
This creates a dictionary of Adam optimizers, each with its own hyperparameters. This creates a dictionary of Adam optimizers, each with its own hyperparameters.
Args: Args:
lr (`float`, *optional*, defaults to 0.001): lr: Default learning rate (used if not specified for a group)
Default learning rate, used for a group unless overridden in `optimizer_groups`. weight_decay: Default weight decay (used if not specified for a group)
weight_decay (`float`, *optional*, defaults to 0.0): optimizer_groups: Dictionary mapping parameter group names to their hyperparameters
Default weight decay, used for a group unless overridden in `optimizer_groups`. grad_clip_norm: Gradient clipping norm
grad_clip_norm (`float`, *optional*, defaults to 10.0):
Maximum gradient norm; gradients are clipped to this value before each optimizer step.
optimizer_groups (`dict[str, dict[str, Any]]`, *optional*):
Per-group hyperparameter overrides (`lr`, `betas`, `eps`, `weight_decay`), keyed by group name.
""" """
lr: float = 1e-3 lr: float = 1e-3
@@ -353,7 +252,6 @@ class MultiAdamConfig(OptimizerConfig):
@property @property
def builds_multiple_optimizers(self) -> bool: def builds_multiple_optimizers(self) -> bool:
"""`bool`: Always `True`; `build()` returns a dict of optimizers, one per parameter group."""
return True return True
def build(self, params: OptimizerParams) -> dict[str, torch.optim.Optimizer]: def build(self, params: OptimizerParams) -> dict[str, torch.optim.Optimizer]:
@@ -398,10 +296,8 @@ def save_optimizer_state(
"""Save optimizer state to disk (non-sharded runs; sharded runs use the DCP channel). """Save optimizer state to disk (non-sharded runs; sharded runs use the DCP channel).
Args: Args:
optimizer (`torch.optim.Optimizer | dict[str, torch.optim.Optimizer]`): optimizer: Either a single optimizer or a dictionary of optimizers.
Either a single optimizer or a dictionary of optimizers. save_dir: Directory to save the optimizer state.
save_dir (`Path`):
Directory to save the optimizer state.
""" """
if isinstance(optimizer, dict): if isinstance(optimizer, dict):
# Handle dictionary of optimizers # Handle dictionary of optimizers
@@ -429,14 +325,11 @@ def load_optimizer_state(
"""Load optimizer state from disk. """Load optimizer state from disk.
Args: Args:
optimizer (`torch.optim.Optimizer | dict[str, torch.optim.Optimizer]`): optimizer: Either a single optimizer or a dictionary of optimizers.
Either a single optimizer or a dictionary of optimizers. save_dir: Directory to load the optimizer state from.
save_dir (`Path`):
Directory to load the optimizer state from.
Returns: Returns:
`torch.optim.Optimizer | dict[str, torch.optim.Optimizer]`: The updated optimizer(s) with loaded The updated optimizer(s) with loaded state.
state.
""" """
if isinstance(optimizer, dict): if isinstance(optimizer, dict):
# Handle dictionary of optimizers # Handle dictionary of optimizers
-112
View File
@@ -36,64 +36,24 @@ else:
@dataclass @dataclass
class LRSchedulerConfig(draccus.ChoiceRegistry, abc.ABC): class LRSchedulerConfig(draccus.ChoiceRegistry, abc.ABC):
"""Base configuration shared by every learning rate scheduler.
Concrete schedulers subclass this and register themselves with
`@LRSchedulerConfig.register_subclass("name")`, which is what makes `--scheduler.type=name` work on the
command line.
Args:
num_warmup_steps (`int | None`):
Number of steps over which the learning rate ramps up from 0 before the scheduler's own
behavior takes over. `None` disables warmup.
"""
num_warmup_steps: int | None num_warmup_steps: int | None
@property @property
def type(self) -> str: def type(self) -> str:
"""Return the registered name this config was registered under.
Returns:
`str`: The name passed to `@LRSchedulerConfig.register_subclass`, e.g. `"diffuser"`.
"""
return self.get_choice_name(self.__class__) return self.get_choice_name(self.__class__)
@abc.abstractmethod @abc.abstractmethod
def build(self, optimizer: Optimizer, num_training_steps: int) -> LRScheduler | None: def build(self, optimizer: Optimizer, num_training_steps: int) -> LRScheduler | None:
"""Build the scheduler for a given optimizer and training length.
Args:
optimizer (`Optimizer`):
The optimizer whose learning rate the scheduler will adjust.
num_training_steps (`int`):
Total number of training steps, used to compute decay/annealing schedules.
Returns:
`LRScheduler | None`: The built scheduler.
"""
raise NotImplementedError raise NotImplementedError
@LRSchedulerConfig.register_subclass("diffuser") @LRSchedulerConfig.register_subclass("diffuser")
@dataclass @dataclass
class DiffuserSchedulerConfig(LRSchedulerConfig): class DiffuserSchedulerConfig(LRSchedulerConfig):
"""A [`diffusers`](https://huggingface.co/docs/diffusers) learning rate schedule.
Args:
num_warmup_steps (`int`, *optional*):
Number of steps over which the learning rate ramps up from 0. `None` disables warmup.
name (`str`, *optional*, defaults to `"cosine"`):
Name of the `diffusers` schedule to build, e.g. `"cosine"`, `"linear"`, `"constant"`. See
[`diffusers.optimization.get_scheduler`](https://huggingface.co/docs/diffusers/api/schedulers/overview)
for the full list.
"""
name: str = "cosine" name: str = "cosine"
num_warmup_steps: int | None = None num_warmup_steps: int | None = None
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR: def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
"""See [`~optim.schedulers.LRSchedulerConfig.build`]. Delegates to `diffusers.optimization.get_scheduler`."""
require_package("diffusers", extra="diffusion") require_package("diffusers", extra="diffusion")
kwargs = {**asdict(self), "num_training_steps": num_training_steps, "optimizer": optimizer} kwargs = {**asdict(self), "num_training_steps": num_training_steps, "optimizer": optimizer}
@@ -103,31 +63,12 @@ class DiffuserSchedulerConfig(LRSchedulerConfig):
@LRSchedulerConfig.register_subclass("vqbet") @LRSchedulerConfig.register_subclass("vqbet")
@dataclass @dataclass
class VQBeTSchedulerConfig(LRSchedulerConfig): class VQBeTSchedulerConfig(LRSchedulerConfig):
"""Used to train VQ-BeT: constant LR during VQ-VAE pretraining, then warmup and cosine decay.
Args:
num_warmup_steps (`int`):
Number of steps over which the learning rate ramps up from 0, counted from the end of VQ-VAE
pretraining.
num_vqvae_training_steps (`int`):
Number of initial steps spent pretraining the VQ-VAE, during which the LR stays at its peak.
num_cycles (`float`, *optional*, defaults to 0.5):
Number of cosine cycles in the decay phase; 0.5 decays smoothly to 0 by the end of training.
"""
num_warmup_steps: int num_warmup_steps: int
num_vqvae_training_steps: int num_vqvae_training_steps: int
num_cycles: float = 0.5 num_cycles: float = 0.5
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR: def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
"""See [`~optim.schedulers.LRSchedulerConfig.build`].
Holds the LR at its peak during VQ-VAE pretraining, then applies linear warmup followed by cosine
decay for the remaining steps.
"""
def lr_lambda(current_step): def lr_lambda(current_step):
"""Return the LR multiplier for `current_step`, per the VQ-BeT schedule."""
if current_step < self.num_vqvae_training_steps: if current_step < self.num_vqvae_training_steps:
return float(1) return float(1)
else: else:
@@ -149,20 +90,14 @@ class ConstantWithWarmupSchedulerConfig(LRSchedulerConfig):
Mirrors the ``warmup_constant_lambda`` used by LingBot-VA (upstream ``wan_va/train.py``): Mirrors the ``warmup_constant_lambda`` used by LingBot-VA (upstream ``wan_va/train.py``):
the LR ramps linearly from 0 to the peak over ``num_warmup_steps`` steps, then stays flat. the LR ramps linearly from 0 to the peak over ``num_warmup_steps`` steps, then stays flat.
Args:
num_warmup_steps (`int`, *optional*, defaults to 1000):
Number of steps over which the learning rate ramps up from 0 to its peak.
""" """
num_warmup_steps: int = 1000 num_warmup_steps: int = 1000
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR: def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
"""See [`~optim.schedulers.LRSchedulerConfig.build`]."""
warmup_steps = self.num_warmup_steps or 0 warmup_steps = self.num_warmup_steps or 0
def lr_lambda(current_step): def lr_lambda(current_step):
"""Return the LR multiplier for `current_step`: linear ramp, then constant `1.0`."""
if current_step < warmup_steps: if current_step < warmup_steps:
return float(current_step) / float(max(1, warmup_steps)) return float(current_step) / float(max(1, warmup_steps))
return 1.0 return 1.0
@@ -176,19 +111,12 @@ class CosineAnnealingWithWarmupSchedulerConfig(LRSchedulerConfig):
"""Linear warmup followed by cosine annealing from the peak LR to zero. """Linear warmup followed by cosine annealing from the peak LR to zero.
Used by EVO1; the annealing phase always spans the remaining training steps. Used by EVO1; the annealing phase always spans the remaining training steps.
Args:
num_warmup_steps (`int`):
Number of steps over which the learning rate ramps up from 0 to its peak.
""" """
num_warmup_steps: int num_warmup_steps: int
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR: def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
"""See [`~optim.schedulers.LRSchedulerConfig.build`]."""
def lr_lambda(current_step: int) -> float: def lr_lambda(current_step: int) -> float:
"""Return the LR multiplier for `current_step`: linear warmup, then cosine annealing to 0."""
if current_step < self.num_warmup_steps: if current_step < self.num_warmup_steps:
return current_step / max(1, self.num_warmup_steps) return current_step / max(1, self.num_warmup_steps)
progress = (current_step - self.num_warmup_steps) / max( progress = (current_step - self.num_warmup_steps) / max(
@@ -206,18 +134,6 @@ class CosineDecayWithWarmupSchedulerConfig(LRSchedulerConfig):
Automatically scales warmup and decay steps if num_training_steps < num_decay_steps. Automatically scales warmup and decay steps if num_training_steps < num_decay_steps.
This ensures the learning rate schedule completes properly even with shorter training runs. This ensures the learning rate schedule completes properly even with shorter training runs.
Args:
num_warmup_steps (`int`):
Number of steps over which the learning rate ramps up from `peak_lr / (num_warmup_steps + 1)`
to `peak_lr`.
num_decay_steps (`int`):
Number of steps over which the learning rate decays from `peak_lr` to `decay_lr`. Scaled down
automatically if `num_training_steps` is shorter than this.
peak_lr (`float`):
Learning rate reached at the end of warmup.
decay_lr (`float`):
Learning rate reached at the end of decay.
""" """
num_warmup_steps: int num_warmup_steps: int
@@ -226,11 +142,6 @@ class CosineDecayWithWarmupSchedulerConfig(LRSchedulerConfig):
decay_lr: float decay_lr: float
def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR: def build(self, optimizer: Optimizer, num_training_steps: int) -> LambdaLR:
"""See [`~optim.schedulers.LRSchedulerConfig.build`].
If `num_training_steps` is shorter than `num_decay_steps`, scales `num_warmup_steps` and
`num_decay_steps` down proportionally so the schedule still completes.
"""
# Auto-scale scheduler parameters if training steps are shorter than configured decay steps # Auto-scale scheduler parameters if training steps are shorter than configured decay steps
actual_warmup_steps = self.num_warmup_steps actual_warmup_steps = self.num_warmup_steps
actual_decay_steps = self.num_decay_steps actual_decay_steps = self.num_decay_steps
@@ -250,17 +161,13 @@ class CosineDecayWithWarmupSchedulerConfig(LRSchedulerConfig):
) )
def lr_lambda(current_step): def lr_lambda(current_step):
"""Return the LR multiplier for `current_step`: linear warmup, then cosine decay."""
def linear_warmup_schedule(current_step): def linear_warmup_schedule(current_step):
"""Return the LR multiplier during warmup, ramping from `1 / (warmup + 1)` to 1."""
if current_step <= 0: if current_step <= 0:
return 1 / (actual_warmup_steps + 1) return 1 / (actual_warmup_steps + 1)
frac = 1 - current_step / actual_warmup_steps frac = 1 - current_step / actual_warmup_steps
return (1 / (actual_warmup_steps + 1) - 1) * frac + 1 return (1 / (actual_warmup_steps + 1) - 1) * frac + 1
def cosine_decay_schedule(current_step): def cosine_decay_schedule(current_step):
"""Return the LR multiplier during decay, from 1 down to `decay_lr / peak_lr`."""
step = min(current_step, actual_decay_steps) step = min(current_step, actual_decay_steps)
cosine_decay = 0.5 * (1 + math.cos(math.pi * step / actual_decay_steps)) cosine_decay = 0.5 * (1 + math.cos(math.pi * step / actual_decay_steps))
alpha = self.decay_lr / self.peak_lr alpha = self.decay_lr / self.peak_lr
@@ -276,30 +183,11 @@ class CosineDecayWithWarmupSchedulerConfig(LRSchedulerConfig):
def save_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> None: def save_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> None:
"""Save a scheduler's state to disk.
Args:
scheduler (`LRScheduler`):
The scheduler whose state to save.
save_dir (`Path`):
Directory to save the scheduler state.
"""
state_dict = scheduler.state_dict() state_dict = scheduler.state_dict()
write_json(state_dict, save_dir / SCHEDULER_STATE) write_json(state_dict, save_dir / SCHEDULER_STATE)
def load_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> LRScheduler: def load_scheduler_state(scheduler: LRScheduler, save_dir: Path) -> LRScheduler:
"""Load a scheduler's state from disk.
Args:
scheduler (`LRScheduler`):
The scheduler to load state into.
save_dir (`Path`):
Directory to load the scheduler state from.
Returns:
`LRScheduler`: The same scheduler, with its state loaded.
"""
state_dict = deserialize_json_into_object(save_dir / SCHEDULER_STATE, scheduler.state_dict()) state_dict = deserialize_json_into_object(save_dir / SCHEDULER_STATE, scheduler.state_dict())
scheduler.load_state_dict(state_dict) scheduler.load_state_dict(state_dict)
return scheduler return scheduler
+1 -1
View File
@@ -60,7 +60,7 @@ PATH_TO_LEROBOT = PATH_TO_REPO / "src" / "lerobot"
# Modules whose public objects are checked. Add a module here once its docstrings follow the standard. # Modules whose public objects are checked. Add a module here once its docstrings follow the standard.
MODULES_TO_CHECK = [ MODULES_TO_CHECK = [
"lerobot.robots", "lerobot.robots",
"lerobot.optim", "lerobot.configs",
] ]
# Objects that do not yet follow the standard, so the check can be green from day one. Removing an entry # Objects that do not yet follow the standard, so the check can be green from day one. Removing an entry