| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| """Train a policy. |
| |
| Requires: pip install 'lerobot[training]' (includes dataset + accelerate + wandb extras) |
| """ |
|
|
| import dataclasses |
| import logging |
| import sys |
| import time |
| from contextlib import nullcontext |
| from pprint import pformat |
| from typing import TYPE_CHECKING, Any |
|
|
| if TYPE_CHECKING: |
| from accelerate import Accelerator |
|
|
| import torch |
| from termcolor import colored |
| from torch.optim import Optimizer |
| from tqdm import tqdm |
|
|
| from lerobot.common.train_utils import ( |
| gather_fsdp_state_dicts, |
| get_step_checkpoint_dir, |
| get_step_identifier, |
| load_fsdp_optimizer_state, |
| load_training_batch_size, |
| load_training_num_processes, |
| load_training_state, |
| push_checkpoint_to_hub, |
| save_checkpoint, |
| update_last_checkpoint, |
| ) |
| from lerobot.common.wandb_utils import WandBLogger |
| from lerobot.configs import JobConfig, parser |
| from lerobot.configs.train import TrainPipelineConfig |
| from lerobot.datasets import EpisodeAwareSampler, compute_sampler_state |
| from lerobot.datasets.factory import make_train_eval_datasets |
| from lerobot.envs import close_envs, make_env, make_env_pre_post_processors |
| from lerobot.jobs import submit_to_hf |
| from lerobot.optim.factory import make_optimizer_and_scheduler |
| from lerobot.policies import PreTrainedPolicy, make_policy, make_pre_post_processors |
| from lerobot.rewards import make_reward_pre_post_processors |
| from lerobot.utils.collate import lerobot_collate_fn |
| from lerobot.utils.import_utils import register_third_party_plugins |
| from lerobot.utils.logging_utils import AverageMeter, MetricsTracker |
| from lerobot.utils.random_utils import set_seed |
| from lerobot.utils.utils import ( |
| cycle, |
| format_big_number, |
| has_method, |
| init_logging, |
| inside_slurm, |
| ) |
|
|
| from .lerobot_eval import eval_policy_all |
|
|
|
|
| def update_policy( |
| train_metrics: MetricsTracker, |
| policy: PreTrainedPolicy, |
| batch: Any, |
| optimizer: Optimizer, |
| grad_clip_norm: float, |
| accelerator: "Accelerator", |
| lr_scheduler=None, |
| lock=None, |
| sample_weighter=None, |
| ) -> tuple[MetricsTracker, dict | None]: |
| """ |
| Performs a single training step to update the policy's weights. |
| |
| This function executes the forward and backward passes, clips gradients, and steps the optimizer and |
| learning rate scheduler. Accelerator handles mixed-precision training automatically. |
| |
| Args: |
| train_metrics: A MetricsTracker instance to record training statistics. |
| policy: The policy model to be trained. |
| batch: A batch of training data. |
| optimizer: The optimizer used to update the policy's parameters. |
| grad_clip_norm: The maximum norm for gradient clipping. |
| accelerator: The Accelerator instance for distributed training and mixed precision. |
| lr_scheduler: An optional learning rate scheduler. |
| lock: An optional lock for thread-safe optimizer updates. |
| sample_weighter: Optional SampleWeighter instance for per-sample loss weighting. |
| |
| Returns: |
| A tuple containing: |
| - The updated MetricsTracker with new statistics for this step. |
| - A dictionary of outputs from the policy's forward pass, for logging purposes. |
| """ |
| start_time = time.perf_counter() |
| policy.train() |
|
|
| if torch.cuda.is_available(): |
| torch.cuda.reset_peak_memory_stats() |
|
|
| |
| sample_weights = None |
| weight_stats = None |
| if sample_weighter is not None: |
| sample_weights, weight_stats = sample_weighter.compute_batch_weights(batch) |
|
|
| |
| with accelerator.autocast(): |
| if sample_weights is not None: |
| |
| |
| per_sample_loss, output_dict = policy.forward(batch, reduction="none") |
|
|
| |
| |
| |
| |
| epsilon = 1e-6 |
| loss = (per_sample_loss * sample_weights).sum() / (sample_weights.sum() + epsilon) |
|
|
| |
| if output_dict is None: |
| output_dict = {} |
| for key, value in weight_stats.items(): |
| output_dict[f"sample_weight_{key}"] = value |
| else: |
| loss, output_dict = policy.forward(batch) |
|
|
| |
|
|
| |
| accelerator.backward(loss) |
|
|
| |
| if grad_clip_norm > 0: |
| grad_norm = accelerator.clip_grad_norm_(policy.parameters(), grad_clip_norm) |
| else: |
| grad_norm = torch.nn.utils.clip_grad_norm_( |
| policy.parameters(), float("inf"), error_if_nonfinite=False |
| ) |
|
|
| |
| with lock if lock is not None else nullcontext(): |
| optimizer.step() |
|
|
| optimizer.zero_grad() |
|
|
| |
| if lr_scheduler is not None: |
| lr_scheduler.step() |
|
|
| |
| if has_method(accelerator.unwrap_model(policy, keep_fp32_wrapper=True), "update"): |
| accelerator.unwrap_model(policy, keep_fp32_wrapper=True).update() |
|
|
| train_metrics.loss = loss.item() |
| train_metrics.grad_norm = grad_norm.item() |
| train_metrics.lr = optimizer.param_groups[0]["lr"] |
| train_metrics.update_s = time.perf_counter() - start_time |
| if torch.cuda.is_available(): |
| train_metrics.gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3) |
| return train_metrics, output_dict |
|
|
|
|
| @parser.wrap() |
| def train(cfg: TrainPipelineConfig, accelerator: "Accelerator | None" = None): |
| """ |
| Main function to train a policy. |
| |
| This function orchestrates the entire training pipeline, including: |
| - Setting up logging, seeding, and device configuration. |
| - Creating the dataset, evaluation environment (if applicable), policy, and optimizer. |
| - Handling resumption from a checkpoint. |
| - Running the main training loop, which involves fetching data batches and calling `update_policy`. |
| - Periodically logging metrics, saving model checkpoints, and evaluating the policy. |
| - Pushing the final trained model to the Hugging Face Hub if configured. |
| |
| Args: |
| cfg: A `TrainPipelineConfig` object containing all training configurations. |
| accelerator: Optional Accelerator instance. If None, one will be created automatically. |
| """ |
| if cfg.job.is_remote: |
| return submit_to_hf(cfg) |
|
|
| from lerobot.utils.import_utils import require_package |
|
|
| require_package("accelerate", extra="training") |
| from accelerate import Accelerator |
| from accelerate.utils import DistributedDataParallelKwargs, DistributedType |
|
|
| cfg.validate() |
|
|
| |
| |
| |
| |
| if accelerator is None: |
| ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) |
| |
| |
| force_cpu = cfg.trainable_config.device == "cpu" |
| |
| policy_dtype = getattr(cfg.trainable_config, "dtype", None) |
| mixed_precision = {"bfloat16": "bf16", "float16": "fp16", "float32": "no"}.get(policy_dtype) |
| accelerator = Accelerator( |
| step_scheduler_with_optimizer=False, |
| mixed_precision=mixed_precision, |
| kwargs_handlers=[ddp_kwargs], |
| cpu=force_cpu, |
| ) |
|
|
| init_logging(accelerator=accelerator) |
|
|
| |
| |
| is_main_process = accelerator.is_main_process |
|
|
| |
| if is_main_process: |
| logging.info(pformat(cfg.to_dict())) |
|
|
| |
| if cfg.wandb.enable and cfg.wandb.project and is_main_process: |
| wandb_logger = WandBLogger(cfg) |
| else: |
| wandb_logger = None |
| if is_main_process: |
| logging.info(colored("Logs will be saved locally.", "yellow", attrs=["bold"])) |
|
|
| if cfg.seed is not None: |
| set_seed(cfg.seed, accelerator=accelerator) |
|
|
| |
| device = accelerator.device |
| if cfg.cudnn_deterministic: |
| torch.backends.cudnn.deterministic = True |
| torch.backends.cudnn.benchmark = False |
| else: |
| torch.backends.cudnn.benchmark = True |
| torch.backends.cuda.matmul.allow_tf32 = True |
|
|
| |
| |
| |
| if is_main_process: |
| logging.info("Creating dataset") |
| dataset, eval_dataset = make_train_eval_datasets(cfg) |
|
|
| accelerator.wait_for_everyone() |
|
|
| |
| if not is_main_process: |
| dataset, eval_dataset = make_train_eval_datasets(cfg) |
|
|
| |
| |
| |
| eval_env = None |
| if cfg.env_eval_freq > 0 and cfg.env is not None and is_main_process: |
| logging.info("Creating env") |
| eval_env = make_env(cfg.env, n_envs=cfg.eval.batch_size, use_async_envs=cfg.eval.use_async_envs) |
|
|
| if cfg.is_reward_model_training: |
| if is_main_process: |
| logging.info("Creating reward model") |
| from lerobot.rewards import make_reward_model |
|
|
| policy = make_reward_model( |
| cfg=cfg.reward_model, |
| dataset_stats=dataset.meta.stats, |
| dataset_meta=dataset.meta, |
| ) |
| if not policy.is_trainable: |
| raise ValueError( |
| f"Reward model '{policy.name}' is zero-shot and cannot be trained via lerobot-train. " |
| "Use it directly for inference via compute_reward() (e.g. offline precompute)." |
| ) |
| else: |
| if is_main_process: |
| logging.info("Creating policy") |
| policy = make_policy( |
| cfg=cfg.policy, |
| ds_meta=dataset.meta, |
| rename_map=cfg.rename_map, |
| ) |
|
|
| if cfg.peft is not None: |
| if cfg.is_reward_model_training: |
| raise ValueError("PEFT is only supported for policy training. ") |
| from peft import PeftModel |
|
|
| if isinstance(policy, PeftModel): |
| logging.info("PEFT adapter already loaded from checkpoint, skipping wrap_with_peft.") |
| else: |
| logging.info("Using PEFT! Wrapping model.") |
| peft_cli_overrides = dataclasses.asdict(cfg.peft) |
| policy = policy.wrap_with_peft(peft_cli_overrides=peft_cli_overrides) |
|
|
| |
| accelerator.wait_for_everyone() |
|
|
| active_cfg = cfg.trainable_config |
| processor_pretrained_path = active_cfg.pretrained_path |
|
|
| processor_kwargs = {} |
| if (processor_pretrained_path and not cfg.resume) or not processor_pretrained_path: |
| processor_kwargs["dataset_stats"] = dataset.meta.stats |
|
|
| if cfg.is_reward_model_training: |
| processor_kwargs["dataset_meta"] = dataset.meta |
|
|
| if not cfg.is_reward_model_training and processor_pretrained_path is not None: |
| preprocessor_overrides = { |
| "device_processor": {"device": device.type}, |
| "normalizer_processor": { |
| "stats": dataset.meta.stats, |
| "features": {**policy.config.input_features, **policy.config.output_features}, |
| "norm_map": policy.config.normalization_mapping, |
| }, |
| "rename_observations_processor": {"rename_map": cfg.rename_map}, |
| } |
| postprocessor_overrides = { |
| "unnormalizer_processor": { |
| "stats": dataset.meta.stats, |
| "features": policy.config.output_features, |
| "norm_map": policy.config.normalization_mapping, |
| }, |
| } |
| if getattr(active_cfg, "use_relative_actions", False): |
| preprocessor_overrides["relative_actions_processor"] = { |
| "enabled": True, |
| "exclude_joints": getattr(active_cfg, "relative_exclude_joints", []), |
| "action_names": getattr(active_cfg, "action_feature_names", None), |
| } |
| postprocessor_overrides["absolute_actions_processor"] = {"enabled": True} |
| processor_kwargs["preprocessor_overrides"] = preprocessor_overrides |
| processor_kwargs["postprocessor_overrides"] = postprocessor_overrides |
|
|
| if cfg.is_reward_model_training: |
| preprocessor, postprocessor = make_reward_pre_post_processors( |
| cfg.reward_model, |
| **processor_kwargs, |
| ) |
| else: |
| preprocessor, postprocessor = make_pre_post_processors( |
| policy_cfg=cfg.policy, |
| pretrained_path=processor_pretrained_path, |
| pretrained_revision=getattr(cfg.policy, "pretrained_revision", None), |
| **processor_kwargs, |
| ) |
|
|
| if is_main_process: |
| logging.info("Creating optimizer and scheduler") |
| optimizer, lr_scheduler = make_optimizer_and_scheduler(cfg, policy) |
|
|
| |
| sample_weighter = None |
| if cfg.sample_weighting is not None: |
| from lerobot.utils.sample_weighting import make_sample_weighter |
|
|
| if is_main_process: |
| logging.info(f"Creating sample weighter: {cfg.sample_weighting.type}") |
| sample_weighter = make_sample_weighter( |
| cfg.sample_weighting, |
| policy, |
| device, |
| dataset_root=cfg.dataset.root, |
| dataset_repo_id=cfg.dataset.repo_id, |
| ) |
|
|
| step = 0 |
|
|
| if cfg.resume: |
| |
| |
| is_fsdp = accelerator.distributed_type == DistributedType.FSDP |
| step, optimizer, lr_scheduler = load_training_state( |
| cfg.checkpoint_path, optimizer, lr_scheduler, load_optimizer=not is_fsdp |
| ) |
|
|
| num_learnable_params = sum(p.numel() for p in policy.parameters() if p.requires_grad) |
| num_total_params = sum(p.numel() for p in policy.parameters()) |
|
|
| if is_main_process: |
| logging.info(colored("Output dir:", "yellow", attrs=["bold"]) + f" {cfg.output_dir}") |
| if cfg.env is not None: |
| logging.info(f"{cfg.env.task=}") |
| logging.info("Creating environment processors") |
| env_preprocessor, env_postprocessor = make_env_pre_post_processors( |
| env_cfg=cfg.env, policy_cfg=cfg.policy |
| ) |
| logging.info(f"{cfg.steps=} ({format_big_number(cfg.steps)})") |
| logging.info(f"{dataset.num_frames=} ({format_big_number(dataset.num_frames)})") |
| logging.info(f"{dataset.num_episodes=}") |
| num_processes = accelerator.num_processes |
| effective_bs = cfg.batch_size * num_processes |
| logging.info(f"Effective batch size: {cfg.batch_size} x {num_processes} = {effective_bs}") |
| logging.info(f"{num_learnable_params=} ({format_big_number(num_learnable_params)})") |
| logging.info(f"{num_total_params=} ({format_big_number(num_total_params)})") |
|
|
| |
| if not cfg.dataset.streaming: |
| |
| |
| |
| |
| shuffle = False |
| sampler = EpisodeAwareSampler( |
| dataset.meta.episodes["dataset_from_index"], |
| dataset.meta.episodes["dataset_to_index"], |
| episode_indices_to_use=dataset.episodes, |
| drop_n_last_frames=getattr(active_cfg, "drop_n_last_frames", 0), |
| shuffle=True, |
| seed=cfg.seed if cfg.seed is not None else 0, |
| absolute_to_relative_idx=dataset.absolute_to_relative_idx, |
| ) |
| if cfg.resume and step > 0: |
| |
| |
| |
| saved_num_processes = load_training_num_processes(cfg.checkpoint_path) |
| saved_batch_size = load_training_batch_size(cfg.checkpoint_path) |
| ckpt_num_processes = saved_num_processes or accelerator.num_processes |
| ckpt_batch_size = saved_batch_size or cfg.batch_size |
| if is_main_process and saved_num_processes not in (None, accelerator.num_processes): |
| logging.warning( |
| f"Resuming with num_processes={accelerator.num_processes} but the checkpoint was " |
| f"written with num_processes={saved_num_processes}. The data order resumes at the " |
| "right epoch/offset, but per-rank sample-exactness requires the same world size." |
| ) |
| if is_main_process and saved_batch_size not in (None, cfg.batch_size): |
| logging.warning( |
| f"Resuming with batch_size={cfg.batch_size} but the checkpoint was written with " |
| f"batch_size={saved_batch_size}. The data order resumes at the right epoch/offset, " |
| "but per-rank sample-exactness requires the same batch size." |
| ) |
| sampler_state = compute_sampler_state(step, len(sampler), ckpt_batch_size, ckpt_num_processes) |
| sampler.load_state_dict(sampler_state) |
| if is_main_process: |
| logging.info( |
| f"Resuming data order at epoch {sampler_state['epoch']}, " |
| f"sample {sampler_state['start_index']}" |
| ) |
| else: |
| shuffle = True |
| sampler = None |
|
|
| |
| |
| |
| collate_fn = lerobot_collate_fn if dataset.meta.has_language_columns else None |
| dataloader = torch.utils.data.DataLoader( |
| dataset, |
| num_workers=cfg.num_workers, |
| batch_size=cfg.batch_size, |
| shuffle=shuffle and not cfg.dataset.streaming, |
| sampler=sampler, |
| pin_memory=device.type == "cuda", |
| drop_last=False, |
| collate_fn=collate_fn, |
| prefetch_factor=cfg.prefetch_factor if cfg.num_workers > 0 else None, |
| persistent_workers=cfg.persistent_workers and cfg.num_workers > 0, |
| ) |
|
|
| |
| eval_dataloader = None |
| if eval_dataset is not None: |
| eval_ds = eval_dataset |
| if cfg.max_eval_samples > 0 and hasattr(eval_dataset, "hf_dataset"): |
| task_arr = eval_dataset.hf_dataset.data.column("task_index").to_numpy() |
| unique_tasks = sorted(set(task_arr.tolist())) |
| per_task = max(1, cfg.max_eval_samples // len(unique_tasks)) |
| selected: list[int] = [] |
| for t in unique_tasks: |
| frames = (task_arr == t).nonzero()[0][:per_task] |
| selected.extend(frames.tolist()) |
| eval_ds = torch.utils.data.Subset(eval_dataset, selected) |
|
|
| eval_collate_fn = lerobot_collate_fn if dataset.meta.has_language_columns else None |
| eval_dataloader = torch.utils.data.DataLoader( |
| eval_ds, |
| batch_size=cfg.batch_size, |
| shuffle=False, |
| num_workers=cfg.num_workers, |
| pin_memory=device.type == "cuda", |
| drop_last=False, |
| collate_fn=eval_collate_fn, |
| prefetch_factor=cfg.prefetch_factor if cfg.num_workers > 0 else None, |
| persistent_workers=cfg.persistent_workers and cfg.num_workers > 0, |
| ) |
|
|
| |
| accelerator.wait_for_everyone() |
| if eval_dataloader is not None: |
| policy, optimizer, dataloader, lr_scheduler, eval_dataloader = accelerator.prepare( |
| policy, optimizer, dataloader, lr_scheduler, eval_dataloader |
| ) |
| else: |
| policy, optimizer, dataloader, lr_scheduler = accelerator.prepare( |
| policy, optimizer, dataloader, lr_scheduler |
| ) |
|
|
| |
| |
| if cfg.resume and accelerator.distributed_type == DistributedType.FSDP: |
| load_fsdp_optimizer_state(policy, optimizer, cfg.checkpoint_path) |
|
|
| dl_iter = cycle(dataloader) |
|
|
| policy.train() |
|
|
| train_metrics = { |
| |
| |
| |
| "loss": AverageMeter("loss", ":.3f", reduction="mean"), |
| "grad_norm": AverageMeter("grdn", ":.3f"), |
| "lr": AverageMeter("lr", ":0.1e"), |
| |
| |
| "update_s": AverageMeter("updt_s", ":.3f", reduction="max"), |
| "dataloading_s": AverageMeter("data_s", ":.3f", reduction="max"), |
| |
| "samples_per_s": AverageMeter("smp/s", ":.0f"), |
| } |
| if torch.cuda.is_available(): |
| |
| train_metrics["gpu_mem_gb"] = AverageMeter("mem_gb", ":.2f", reduction="max") |
|
|
| |
| effective_batch_size = cfg.batch_size * accelerator.num_processes |
| train_tracker = MetricsTracker( |
| cfg.batch_size, |
| dataset.num_frames, |
| dataset.num_episodes, |
| train_metrics, |
| initial_step=step, |
| accelerator=accelerator, |
| ) |
|
|
| if is_main_process: |
| progbar = tqdm( |
| total=cfg.steps - step, |
| desc="Training", |
| unit="step", |
| disable=inside_slurm(), |
| position=0, |
| leave=True, |
| ) |
| logging.info( |
| f"Start offline training on a fixed dataset, with effective batch size: {effective_batch_size}" |
| ) |
|
|
| for _ in range(step, cfg.steps): |
| start_time = time.perf_counter() |
| batch = next(dl_iter) |
| for cam_key in dataset.meta.camera_keys: |
| if cam_key in batch and batch[cam_key].dtype == torch.uint8: |
| batch[cam_key] = batch[cam_key].to(dtype=torch.float32) / 255.0 |
| batch = preprocessor(batch) |
| train_tracker.dataloading_s = time.perf_counter() - start_time |
|
|
| train_tracker, output_dict = update_policy( |
| train_tracker, |
| policy, |
| batch, |
| optimizer, |
| cfg.optimizer.grad_clip_norm, |
| accelerator=accelerator, |
| lr_scheduler=lr_scheduler, |
| sample_weighter=sample_weighter, |
| ) |
|
|
| |
| |
| step += 1 |
| if is_main_process: |
| progbar.update(1) |
| train_tracker.step() |
| is_log_step = cfg.log_freq > 0 and step % cfg.log_freq == 0 |
| is_saving_step = step % cfg.save_freq == 0 or step == cfg.steps |
| is_env_eval_step = cfg.env_eval_freq > 0 and step % cfg.env_eval_freq == 0 |
| is_eval_step = cfg.eval_steps > 0 and eval_dataloader is not None and step % cfg.eval_steps == 0 |
|
|
| if is_log_step: |
| |
| train_tracker.reduce_across_ranks() |
| if is_main_process: |
| |
| |
| step_time = train_tracker.update_s.avg + train_tracker.dataloading_s.avg |
| if step_time > 0: |
| train_tracker.samples_per_s = effective_batch_size / step_time |
| logging.info(train_tracker) |
| if wandb_logger: |
| wandb_log_dict = train_tracker.to_dict() |
| if output_dict: |
| wandb_log_dict.update(output_dict) |
| |
| if sample_weighter is not None: |
| weighter_stats = sample_weighter.get_stats() |
| wandb_log_dict.update({f"sample_weighting/{k}": v for k, v in weighter_stats.items()}) |
| wandb_logger.log_dict(wandb_log_dict, step) |
| train_tracker.reset_averages() |
|
|
| if is_eval_step: |
| policy.eval() |
| eval_loss_sum = 0.0 |
| n_eval_batches = 0 |
| with torch.no_grad(), accelerator.autocast(): |
| for eval_batch in eval_dataloader: |
| for cam_key in dataset.meta.camera_keys: |
| if cam_key in eval_batch and eval_batch[cam_key].dtype == torch.uint8: |
| eval_batch[cam_key] = eval_batch[cam_key].to(dtype=torch.float32) / 255.0 |
| eval_batch = preprocessor(eval_batch) |
| loss, _ = policy.forward(eval_batch) |
| eval_loss_sum += loss.item() |
| n_eval_batches += 1 |
| eval_loss = eval_loss_sum / max(n_eval_batches, 1) |
| eval_loss = torch.tensor(eval_loss, device=device) |
| eval_loss = accelerator.reduce(eval_loss, reduction="mean").item() |
| policy.train() |
|
|
| if is_main_process: |
| logging.info(f"step {step}: eval_loss={eval_loss:.4f}") |
| if wandb_logger: |
| wandb_logger.log_dict({"eval_loss": eval_loss}, step=step, mode="eval") |
|
|
| if cfg.save_checkpoint and is_saving_step: |
| |
| |
| |
| is_fsdp = accelerator.distributed_type == DistributedType.FSDP |
| if is_fsdp: |
| model_state_dict, optim_state_dict = gather_fsdp_state_dicts(policy, optimizer) |
| else: |
| model_state_dict, optim_state_dict = None, None |
| if is_main_process: |
| logging.info(f"Checkpoint policy after step {step}") |
| checkpoint_dir = get_step_checkpoint_dir(cfg.output_dir, cfg.steps, step) |
| save_checkpoint( |
| checkpoint_dir=checkpoint_dir, |
| step=step, |
| cfg=cfg, |
| policy=accelerator.unwrap_model(policy), |
| optimizer=optimizer, |
| scheduler=lr_scheduler, |
| preprocessor=preprocessor, |
| postprocessor=postprocessor, |
| num_processes=accelerator.num_processes, |
| batch_size=cfg.batch_size, |
| model_state_dict=model_state_dict, |
| optim_state_dict=optim_state_dict, |
| ) |
| update_last_checkpoint(checkpoint_dir) |
| if cfg.save_checkpoint_to_hub: |
| push_checkpoint_to_hub( |
| checkpoint_dir, |
| cfg.policy.repo_id, |
| private=cfg.policy.private, |
| ) |
| if wandb_logger: |
| wandb_logger.log_policy(checkpoint_dir) |
|
|
| accelerator.wait_for_everyone() |
|
|
| if cfg.env and is_env_eval_step: |
| if is_main_process: |
| step_id = get_step_identifier(step, cfg.steps) |
| logging.info(f"Eval policy at step {step}") |
| with torch.no_grad(), accelerator.autocast(): |
| eval_info = eval_policy_all( |
| envs=eval_env, |
| policy=accelerator.unwrap_model(policy), |
| env_preprocessor=env_preprocessor, |
| env_postprocessor=env_postprocessor, |
| preprocessor=preprocessor, |
| postprocessor=postprocessor, |
| n_episodes=cfg.eval.n_episodes, |
| videos_dir=cfg.output_dir / "eval" / f"videos_step_{step_id}", |
| max_episodes_rendered=4, |
| start_seed=cfg.seed, |
| max_parallel_tasks=cfg.env.max_parallel_tasks, |
| ) |
| |
| aggregated = eval_info["overall"] |
|
|
| |
| for suite, suite_info in eval_info.items(): |
| logging.info("Suite %s aggregated: %s", suite, suite_info) |
|
|
| |
| eval_metrics = { |
| "avg_sum_reward": AverageMeter("∑rwrd", ":.3f"), |
| "pc_success": AverageMeter("success", ":.1f"), |
| "eval_s": AverageMeter("eval_s", ":.3f"), |
| } |
| eval_tracker = MetricsTracker( |
| cfg.batch_size, |
| dataset.num_frames, |
| dataset.num_episodes, |
| eval_metrics, |
| initial_step=step, |
| accelerator=accelerator, |
| ) |
| eval_tracker.eval_s = aggregated.pop("eval_s") |
| eval_tracker.avg_sum_reward = aggregated.pop("avg_sum_reward") |
| eval_tracker.pc_success = aggregated.pop("pc_success") |
| if wandb_logger: |
| wandb_log_dict = {**eval_tracker.to_dict(), **eval_info} |
| wandb_logger.log_dict(wandb_log_dict, step, mode="eval") |
| for video_idx, video_path in enumerate(eval_info["overall"].get("video_paths", [])): |
| wandb_logger.log_video(video_path, step, mode="eval", name=f"video_{video_idx}") |
|
|
| accelerator.wait_for_everyone() |
|
|
| if is_main_process: |
| progbar.close() |
|
|
| if eval_env: |
| close_envs(eval_env) |
|
|
| is_fsdp = accelerator.distributed_type == DistributedType.FSDP |
| model_state_dict = accelerator.get_state_dict(policy) if is_fsdp else None |
| if is_main_process: |
| logging.info("End of training") |
|
|
| if getattr(active_cfg, "push_to_hub", False): |
| unwrapped_model = accelerator.unwrap_model(policy) |
| |
| if not cfg.is_reward_model_training and cfg.policy.use_peft: |
| unwrapped_model.push_model_to_hub(cfg, peft_model=unwrapped_model, dataset_meta=dataset.meta) |
| else: |
| unwrapped_model.push_model_to_hub(cfg, state_dict=model_state_dict, dataset_meta=dataset.meta) |
| preprocessor.push_to_hub(active_cfg.repo_id) |
| postprocessor.push_to_hub(active_cfg.repo_id) |
|
|
| |
| accelerator.wait_for_everyone() |
| accelerator.end_training() |
|
|
|
|
| def _remote_target_in_argv() -> bool: |
| """True when the CLI requests a remote HF Jobs run (--job.target=<non-local>).""" |
| target = None |
| args = sys.argv[1:] |
| for i, tok in enumerate(args): |
| if tok == "--job.target" and i + 1 < len(args): |
| target = args[i + 1] |
| elif tok.startswith("--job.target="): |
| target = tok.split("=", 1)[1] |
| return JobConfig.is_remote_target(target) |
|
|
|
|
| def main(): |
| register_third_party_plugins() |
| if _remote_target_in_argv(): |
| |
| |
| |
| logging.getLogger("lerobot.configs.policies").setLevel(logging.ERROR) |
| train() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|