mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-28 20:26:05 +00:00
262 lines
11 KiB
Python
262 lines
11 KiB
Python
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
import abc
|
|
import builtins
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from dataclasses import dataclass, field
|
|
from logging import getLogger
|
|
from pathlib import Path
|
|
from typing import Any, TypeVar
|
|
|
|
import draccus
|
|
from huggingface_hub import hf_hub_download
|
|
from huggingface_hub.constants import CONFIG_NAME
|
|
from huggingface_hub.errors import HfHubHTTPError
|
|
|
|
from lerobot.optim import LRSchedulerConfig, OptimizerConfig
|
|
from lerobot.utils.constants import ACTION, OBS_STATE
|
|
from lerobot.utils.device_utils import auto_select_torch_device, is_amp_available, is_torch_device_available
|
|
from lerobot.utils.hub import HubMixin, extract_commit_hash
|
|
|
|
from .types import FeatureType, PolicyFeature
|
|
|
|
T = TypeVar("T", bound="PreTrainedConfig")
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: ignore[misc,name-defined] #TODO: draccus issue
|
|
"""
|
|
Base configuration class for policy models.
|
|
|
|
Args:
|
|
n_obs_steps: Number of environment steps worth of observations to pass to the policy (takes the
|
|
current step and additional steps going back).
|
|
input_features: A dictionary defining the PolicyFeature of the input data for the policy. The key represents
|
|
the input data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
|
output_features: A dictionary defining the PolicyFeature of the output data for the policy. The key represents
|
|
the output data name, and the value is PolicyFeature, which consists of FeatureType and shape attributes.
|
|
normalization_mapping: A dictionary that maps from a str value of FeatureType (e.g., "STATE", "VISUAL") to
|
|
a corresponding NormalizationMode (e.g., NormalizationMode.MIN_MAX)
|
|
"""
|
|
|
|
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)
|
|
output_features: dict[str, PolicyFeature] | None = field(default_factory=dict)
|
|
|
|
device: str | None = None # e.g. "cuda", "cuda:0", "cpu", or "mps"
|
|
# `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
|
|
|
|
# Whether the policy employed PEFT for training.
|
|
use_peft: bool = False
|
|
|
|
push_to_hub: bool = True # type: ignore[assignment] # TODO: use a different name to avoid override
|
|
repo_id: str | None = None
|
|
|
|
# Upload on private repository on the Hugging Face hub.
|
|
private: bool | None = None
|
|
# Add tags to your policy on the hub.
|
|
tags: list[str] | None = None
|
|
# Add tags to your policy on the hub.
|
|
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
|
|
# Optional Hub revision (commit hash, branch, or tag) to pin the pretrained model version.
|
|
pretrained_revision: str | None = None
|
|
|
|
@property
|
|
def _commit_hash(self) -> str | None:
|
|
"""Resolved Hub commit for this runtime load; never serialized."""
|
|
return self.__dict__.get("_runtime_commit_hash")
|
|
|
|
@property
|
|
def _commit_hash_source(self) -> str | None:
|
|
"""Hub repo whose revision resolved to ``_commit_hash``."""
|
|
return self.__dict__.get("_runtime_commit_hash_source")
|
|
|
|
def _set_hub_commit_hash(self, commit_hash: str | None, source: str | None) -> None:
|
|
self.__dict__["_runtime_commit_hash"] = commit_hash
|
|
self.__dict__["_runtime_commit_hash_source"] = source if commit_hash is not None else None
|
|
|
|
def get_hub_revision(self, source: str | Path | None, revision: str | None = None) -> str | None:
|
|
"""Return the pinned revision when ``source`` owns the resolved commit."""
|
|
if self._commit_hash is not None and self._commit_hash_source == str(source):
|
|
return self._commit_hash
|
|
return revision
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.device or not is_torch_device_available(self.device):
|
|
auto_device = auto_select_torch_device()
|
|
logger.warning(f"Device '{self.device}' is not available. Switching to '{auto_device}'.")
|
|
self.device = auto_device.type
|
|
|
|
# Automatically deactivate AMP if necessary
|
|
if self.use_amp and not is_amp_available(self.device):
|
|
logger.warning(
|
|
f"Automatic Mixed Precision (amp) is not available on device '{self.device}'. Deactivating AMP."
|
|
)
|
|
self.use_amp = False
|
|
|
|
@property
|
|
def type(self) -> str:
|
|
choice_name = self.get_choice_name(self.__class__)
|
|
if not isinstance(choice_name, str):
|
|
raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}")
|
|
return choice_name
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def observation_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def action_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def reward_delta_indices(self) -> list | None: # type: ignore[type-arg] #TODO: No implementation
|
|
raise NotImplementedError
|
|
|
|
@abc.abstractmethod
|
|
def get_optimizer_preset(self) -> OptimizerConfig:
|
|
raise NotImplementedError
|
|
|
|
@abc.abstractmethod
|
|
def get_scheduler_preset(self) -> LRSchedulerConfig | None:
|
|
raise NotImplementedError
|
|
|
|
@abc.abstractmethod
|
|
def validate_features(self) -> None:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def robot_state_feature(self) -> PolicyFeature | None:
|
|
if not self.input_features:
|
|
return None
|
|
for ft_name, ft in self.input_features.items():
|
|
if ft.type is FeatureType.STATE and ft_name == OBS_STATE:
|
|
return ft
|
|
return None
|
|
|
|
@property
|
|
def env_state_feature(self) -> PolicyFeature | None:
|
|
if not self.input_features:
|
|
return None
|
|
for _, ft in self.input_features.items():
|
|
if ft.type is FeatureType.ENV:
|
|
return ft
|
|
return None
|
|
|
|
@property
|
|
def image_features(self) -> dict[str, PolicyFeature]:
|
|
if not self.input_features:
|
|
return {}
|
|
return {key: ft for key, ft in self.input_features.items() if ft.type is FeatureType.VISUAL}
|
|
|
|
@property
|
|
def action_feature(self) -> PolicyFeature | None:
|
|
if not self.output_features:
|
|
return None
|
|
for ft_name, ft in self.output_features.items():
|
|
if ft.type is FeatureType.ACTION and ft_name == ACTION:
|
|
return ft
|
|
return None
|
|
|
|
def _save_pretrained(self, save_directory: Path) -> None:
|
|
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
|
draccus.dump(self, f, indent=4)
|
|
|
|
@classmethod
|
|
def from_pretrained(
|
|
cls: builtins.type[T],
|
|
pretrained_name_or_path: str | Path,
|
|
*,
|
|
force_download: bool = False,
|
|
resume_download: bool | None = None,
|
|
proxies: dict[Any, Any] | None = None,
|
|
token: str | bool | None = None,
|
|
cache_dir: str | Path | None = None,
|
|
local_files_only: bool = False,
|
|
revision: str | None = None,
|
|
**policy_kwargs: Any,
|
|
) -> T:
|
|
model_id = str(pretrained_name_or_path)
|
|
config_file: str | None = None
|
|
is_local = Path(model_id).is_dir()
|
|
if is_local:
|
|
if CONFIG_NAME in os.listdir(model_id):
|
|
config_file = os.path.join(model_id, CONFIG_NAME)
|
|
else:
|
|
logger.error(f"{CONFIG_NAME} not found in {Path(model_id).resolve()}")
|
|
else:
|
|
try:
|
|
config_file = hf_hub_download(
|
|
repo_id=model_id,
|
|
filename=CONFIG_NAME,
|
|
revision=revision,
|
|
cache_dir=cache_dir,
|
|
force_download=force_download,
|
|
proxies=proxies,
|
|
resume_download=resume_download,
|
|
token=token,
|
|
local_files_only=local_files_only,
|
|
)
|
|
except HfHubHTTPError as e:
|
|
raise FileNotFoundError(
|
|
f"{CONFIG_NAME} not found on the HuggingFace Hub in {model_id}"
|
|
) from e
|
|
|
|
if config_file is None:
|
|
raise FileNotFoundError(f"{CONFIG_NAME} not found in {model_id}")
|
|
|
|
commit_hash = None if is_local else extract_commit_hash(config_file, revision)
|
|
with open(config_file) as f:
|
|
config = json.load(f)
|
|
# Runtime Hub metadata must never become part of the serialized config schema.
|
|
config.pop("_commit_hash", None)
|
|
config.pop("_commit_hash_source", None)
|
|
|
|
# Resolve the concrete config subclass from the serialized "type" tag, then parse
|
|
# the config (with CLI overrides) directly for that class. The "type" key is
|
|
# stripped because draccus only consumes it when parsing the registry base class.
|
|
policy_type = config.pop("type", None)
|
|
if policy_type is None:
|
|
raise ValueError(f"Missing 'type' field in {CONFIG_NAME} of {model_id}")
|
|
try:
|
|
config_cls = cls.get_choice_class(policy_type)
|
|
except Exception as e:
|
|
raise ValueError(
|
|
f"Policy type '{policy_type}' (from {CONFIG_NAME} of {model_id}) is not registered. "
|
|
f"Available policy types: {cls.get_known_choices()}"
|
|
) from e
|
|
|
|
with tempfile.NamedTemporaryFile("w+", delete=False, suffix=".json") as f:
|
|
json.dump(config, f)
|
|
config_file = f.name
|
|
|
|
cli_overrides = policy_kwargs.pop("cli_overrides", [])
|
|
with draccus.config_type("json"):
|
|
parsed_config = draccus.parse(config_cls, config_file, args=cli_overrides)
|
|
parsed_config._set_hub_commit_hash(commit_hash, model_id)
|
|
return parsed_config
|