mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-26 03:06:01 +00:00
8a3d64033f
* feat(rewards): add RewardModelConfig and PreTrainedRewardModel base classes * refactor(rewards): migrate Classifier from policies/sac/reward_model/ to rewards/classifier/ * refactor(rewards): migrate SARM from policies/sarm/ to rewards/sarm/ * refactor(rewards): add rewards/factory.py and remove reward model code from policies/factory.py * refactor(rewards): update imports and delete old reward model locations * test(rewards): add reward model tests and update existing test imports * fix(rewards): restore full Classifier and SARM implementations * test(rewards): restore missing CUDA and mixed precision classifier processor tests * refactor(lerobot_train.py): remove rabc specific configuration and replace it with a generic samplerweight class in lerobot_train * refactor(lerobot_train.py): add missing sampling weight script * linter + missing files * add testing for sampl weighter * revert some useless changes, improve typing * update docs * add automatic detection of the progress path * remove type exp * improve comment * fix: move rabc.py to rewards/sarm/ and update import paths * refactor(imports): update reward model imports to new module structure * refactor(imports): update reward model imports to reflect new module structure * refactor(imports): conditionally import pandas based on availability * feat(configs): add reward_model field to TrainPipelineConfig and Hub fields to RewardModelConfig * refactor(policies): remove reward model branches from policy factory and __init__ * refactor(rewards): expand __init__ facade and fix SARMConfig __post_init__ crash * feat(train): route reward model training through rewards/factory instead of policies/factory * refactor(train): streamline reward model training logic * fix(rewards): ensure FileNotFoundError is raised for missing config_file * refactor(train): update __get_path_fields__ to include reward_model for config loading * refactor(classifier): remove redundant input normalization in predict_reward method * fix(train): raise ValueError for non-trainable reward models in train function * refactor(pretrained_rm): add model card template * refactor(tests): reward models * refactor(sarm): update reset method and remove unused action prediction methods * refactor(wandb): differentiate tags for reward model and policy training in cfg_to_group function * fix(train): raise ValueError for PEFT usage in reward model training * refactor(rewards): enhance RewardModelConfig with device handling and delta indices properties --------- Co-authored-by: Michel Aractingi <michel.aractingi@huggingface.co>
245 lines
9.7 KiB
Python
245 lines
9.7 KiB
Python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import abc
|
|
import builtins
|
|
import logging
|
|
import os
|
|
from importlib.resources import files
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
|
|
import packaging
|
|
import safetensors
|
|
from huggingface_hub import HfApi, ModelCard, ModelCardData, hf_hub_download
|
|
from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE
|
|
from huggingface_hub.errors import HfHubHTTPError
|
|
from safetensors.torch import load_model as load_model_as_safetensor, save_model as save_model_as_safetensor
|
|
from torch import Tensor, nn
|
|
|
|
from lerobot.configs.rewards import RewardModelConfig
|
|
from lerobot.utils.hub import HubMixin
|
|
|
|
if TYPE_CHECKING:
|
|
from lerobot.configs.train import TrainPipelineConfig
|
|
|
|
T = TypeVar("T", bound="PreTrainedRewardModel")
|
|
|
|
|
|
class PreTrainedRewardModel(nn.Module, HubMixin, abc.ABC):
|
|
"""Base class for reward models."""
|
|
|
|
config_class: None
|
|
name: None
|
|
|
|
def __init__(self, config: RewardModelConfig, *inputs, **kwargs):
|
|
super().__init__()
|
|
if not isinstance(config, RewardModelConfig):
|
|
raise ValueError(
|
|
f"Parameter config in `{self.__class__.__name__}(config)` should be an instance of class "
|
|
"`RewardModelConfig`. To create a model from a pretrained model use "
|
|
f"`model = {self.__class__.__name__}.from_pretrained(PRETRAINED_MODEL_NAME)`"
|
|
)
|
|
self.config = config
|
|
|
|
def __init_subclass__(cls, **kwargs):
|
|
super().__init_subclass__(**kwargs)
|
|
if not getattr(cls, "config_class", None):
|
|
raise TypeError(f"Class {cls.__name__} must define 'config_class'")
|
|
if not getattr(cls, "name", None):
|
|
raise TypeError(f"Class {cls.__name__} must define 'name'")
|
|
|
|
def _save_pretrained(self, save_directory: Path) -> None:
|
|
self.config._save_pretrained(save_directory)
|
|
model_to_save = self.module if hasattr(self, "module") else self
|
|
save_model_as_safetensor(model_to_save, str(save_directory / SAFETENSORS_SINGLE_FILE))
|
|
|
|
@classmethod
|
|
def from_pretrained(
|
|
cls: builtins.type[T],
|
|
pretrained_name_or_path: str | Path,
|
|
*,
|
|
config: RewardModelConfig | None = None,
|
|
force_download: bool = False,
|
|
resume_download: bool | None = None,
|
|
proxies: dict | None = None,
|
|
token: str | bool | None = None,
|
|
cache_dir: str | Path | None = None,
|
|
local_files_only: bool = False,
|
|
revision: str | None = None,
|
|
strict: bool = False,
|
|
**kwargs,
|
|
) -> T:
|
|
"""
|
|
The reward model is set in evaluation mode by default using `reward.eval()` (dropout modules are
|
|
deactivated). To train it, you should first set it back in training mode with `reward.train()`.
|
|
"""
|
|
if config is None:
|
|
config = RewardModelConfig.from_pretrained(
|
|
pretrained_name_or_path=pretrained_name_or_path,
|
|
force_download=force_download,
|
|
resume_download=resume_download,
|
|
proxies=proxies,
|
|
token=token,
|
|
cache_dir=cache_dir,
|
|
local_files_only=local_files_only,
|
|
revision=revision,
|
|
**kwargs,
|
|
)
|
|
model_id = str(pretrained_name_or_path)
|
|
instance = cls(config, **kwargs)
|
|
if os.path.isdir(model_id):
|
|
print("Loading weights from local directory")
|
|
model_file = os.path.join(model_id, SAFETENSORS_SINGLE_FILE)
|
|
reward = cls._load_as_safetensor(instance, model_file, config.device or "cpu", strict)
|
|
else:
|
|
try:
|
|
model_file = hf_hub_download(
|
|
repo_id=model_id,
|
|
filename=SAFETENSORS_SINGLE_FILE,
|
|
revision=revision,
|
|
cache_dir=cache_dir,
|
|
force_download=force_download,
|
|
proxies=proxies,
|
|
resume_download=resume_download,
|
|
token=token,
|
|
local_files_only=local_files_only,
|
|
)
|
|
reward = cls._load_as_safetensor(instance, model_file, config.device or "cpu", strict)
|
|
except HfHubHTTPError as e:
|
|
raise FileNotFoundError(
|
|
f"{SAFETENSORS_SINGLE_FILE} not found on the HuggingFace Hub in {model_id}"
|
|
) from e
|
|
|
|
reward.to(config.device)
|
|
reward.eval()
|
|
return reward
|
|
|
|
@classmethod
|
|
def _load_as_safetensor(cls, model: T, model_file: str, map_location: str, strict: bool) -> T:
|
|
# Create base kwargs
|
|
kwargs = {"strict": strict}
|
|
|
|
# Add device parameter for newer versions that support it
|
|
if packaging.version.parse(safetensors.__version__) >= packaging.version.parse("0.4.3"):
|
|
kwargs["device"] = map_location
|
|
|
|
# Load the model with appropriate kwargs
|
|
missing_keys, unexpected_keys = load_model_as_safetensor(model, model_file, **kwargs)
|
|
if missing_keys:
|
|
logging.warning(f"Missing key(s) when loading model: {missing_keys}")
|
|
if unexpected_keys:
|
|
logging.warning(f"Unexpected key(s) when loading model: {unexpected_keys}")
|
|
|
|
# For older versions, manually move to device if needed
|
|
if "device" not in kwargs and map_location != "cpu":
|
|
logging.warning(
|
|
"Loading model weights on other devices than 'cpu' is not supported natively in your version of safetensors."
|
|
" This means that the model is loaded on 'cpu' first and then copied to the device."
|
|
" This leads to a slower loading time."
|
|
" Please update safetensors to version 0.4.3 or above for improved performance."
|
|
)
|
|
model.to(map_location)
|
|
return model
|
|
|
|
def get_optim_params(self):
|
|
"""
|
|
Returns the reward-model-specific parameters dict to be passed on to the optimizer.
|
|
"""
|
|
return self.parameters()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset any internal state."""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def compute_reward(self, batch: dict[str, Tensor]) -> Tensor:
|
|
"""Compute a scalar reward signal for a batch of observations.
|
|
|
|
Args:
|
|
batch: Dictionary containing at minimum observation tensors.
|
|
May also contain "action", "next_observation.*", etc.
|
|
|
|
Returns:
|
|
Tensor of shape ``(batch_size,)`` with reward values.
|
|
"""
|
|
...
|
|
|
|
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict[str, Any]]:
|
|
"""Training forward pass — override for trainable reward models."""
|
|
raise NotImplementedError(
|
|
f"{self.__class__.__name__} is not trainable. Only use compute_reward() for inference."
|
|
)
|
|
|
|
@property
|
|
def is_trainable(self) -> bool:
|
|
"""Whether this reward model can be trained via ``lerobot-train``.
|
|
|
|
Trainable reward models override :meth:`forward`; zero-shot models
|
|
inherit the base implementation that raises ``NotImplementedError``.
|
|
"""
|
|
return type(self).forward is not PreTrainedRewardModel.forward
|
|
|
|
def push_model_to_hub(self, cfg: "TrainPipelineConfig"):
|
|
api = HfApi()
|
|
repo_id = api.create_repo(
|
|
repo_id=self.config.repo_id, private=self.config.private, exist_ok=True
|
|
).repo_id
|
|
|
|
# Push the files to the repo in a single commit
|
|
with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
saved_path = Path(tmp) / repo_id
|
|
|
|
self.save_pretrained(saved_path) # Calls _save_pretrained and stores model tensors
|
|
|
|
card = self.generate_model_card(
|
|
cfg.dataset.repo_id, self.config.type, self.config.license, self.config.tags
|
|
)
|
|
card.save(str(saved_path / "README.md"))
|
|
|
|
cfg.save_pretrained(saved_path) # Calls _save_pretrained and stores train config
|
|
|
|
commit_info = api.upload_folder(
|
|
repo_id=repo_id,
|
|
repo_type="model",
|
|
folder_path=saved_path,
|
|
commit_message="Upload reward model weights, train config and readme",
|
|
allow_patterns=["*.safetensors", "*.json", "*.yaml", "*.md"],
|
|
ignore_patterns=["*.tmp", "*.log"],
|
|
)
|
|
|
|
logging.info(f"Model pushed to {commit_info.repo_url.url}")
|
|
|
|
def generate_model_card(
|
|
self, dataset_repo_id: str, model_type: str, license: str | None, tags: list[str] | None
|
|
) -> ModelCard:
|
|
card_data = ModelCardData(
|
|
license=license or "apache-2.0",
|
|
library_name="lerobot",
|
|
pipeline_tag="robotics",
|
|
tags=list(set(tags or []).union({"robotics", "lerobot", "reward-model", model_type})),
|
|
model_name=model_type,
|
|
datasets=dataset_repo_id,
|
|
)
|
|
|
|
template_card = (
|
|
files("lerobot.templates")
|
|
.joinpath("lerobot_rewardmodel_modelcard_template.md")
|
|
.read_text(encoding="utf-8")
|
|
)
|
|
card = ModelCard.from_template(card_data, template_str=template_card)
|
|
card.validate()
|
|
return card
|