| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import logging |
| import os |
| import re |
| from glob import glob |
| from pathlib import Path |
|
|
| from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE |
| from termcolor import colored |
|
|
| from lerobot.configs.train import TrainPipelineConfig |
| from lerobot.utils.constants import PRETRAINED_MODEL_DIR |
|
|
|
|
| def cfg_to_group( |
| cfg: TrainPipelineConfig, return_list: bool = False, truncate_tags: bool = False, max_tag_length: int = 64 |
| ) -> list[str] | str: |
| """Return a group name for logging. Optionally returns group name as list.""" |
|
|
| def _maybe_truncate(tag: str) -> str: |
| """Truncate tag to max_tag_length characters if required. |
| |
| wandb rejects tags longer than 64 characters. |
| See: https://github.com/wandb/wandb/blob/main/wandb/sdk/wandb_settings.py |
| """ |
| if len(tag) <= max_tag_length: |
| return tag |
| return tag[:max_tag_length] |
|
|
| if cfg.is_reward_model_training: |
| trainable_tag = f"reward_model:{cfg.reward_model.type}" |
| else: |
| trainable_tag = f"policy:{cfg.policy.type}" |
| lst = [ |
| trainable_tag, |
| f"seed:{cfg.seed}", |
| ] |
| if cfg.dataset is not None: |
| lst.append(f"dataset:{cfg.dataset.repo_id}") |
| if cfg.env is not None: |
| lst.append(f"env:{cfg.env.type}") |
| if truncate_tags: |
| lst = [_maybe_truncate(tag) for tag in lst] |
| return lst if return_list else "-".join(lst) |
|
|
|
|
| def get_wandb_run_id_from_filesystem(log_dir: Path) -> str: |
| |
| paths = glob(str(log_dir / "wandb/latest-run/run-*")) |
| if len(paths) != 1: |
| raise RuntimeError("Couldn't get the previous WandB run ID for run resumption.") |
| match = re.search(r"run-([^\.]+).wandb", paths[0].split("/")[-1]) |
| if match is None: |
| raise RuntimeError("Couldn't get the previous WandB run ID for run resumption.") |
| wandb_run_id = match.groups(0)[0] |
| return wandb_run_id |
|
|
|
|
| def get_safe_wandb_artifact_name(name: str): |
| """WandB artifacts don't accept ":" or "/" in their name.""" |
| return name.replace(":", "_").replace("/", "_") |
|
|
|
|
| class WandBLogger: |
| """A helper class to log object using wandb.""" |
|
|
| def __init__(self, cfg: TrainPipelineConfig): |
| self.cfg = cfg.wandb |
| self.log_dir = cfg.output_dir |
| self.job_name = cfg.job_name |
| self.env_fps = cfg.env.fps if cfg.env else None |
| self._group = cfg_to_group(cfg) |
|
|
| |
| os.environ["WANDB_SILENT"] = "True" |
| import wandb |
|
|
| wandb_run_id = ( |
| cfg.wandb.run_id |
| if cfg.wandb.run_id |
| else get_wandb_run_id_from_filesystem(self.log_dir) |
| if cfg.resume |
| else None |
| ) |
| wandb.init( |
| id=wandb_run_id, |
| project=self.cfg.project, |
| entity=self.cfg.entity, |
| name=self.job_name, |
| notes=self.cfg.notes, |
| tags=cfg_to_group(cfg, return_list=True, truncate_tags=True) if self.cfg.add_tags else None, |
| dir=self.log_dir, |
| config=cfg.to_dict(), |
| |
| save_code=False, |
| |
| job_type="train_eval", |
| resume="must" if cfg.resume else None, |
| mode=self.cfg.mode if self.cfg.mode in ["online", "offline", "disabled"] else "online", |
| ) |
| run_id = wandb.run.id |
| |
| |
| cfg.wandb.run_id = run_id |
| |
| self._wandb_custom_step_key: set[str] | None = None |
| logging.info(colored("Logs will be synced with wandb.", "blue", attrs=["bold"])) |
| logging.info(f"Track this run --> {colored(wandb.run.get_url(), 'yellow', attrs=['bold'])}") |
| self._wandb = wandb |
|
|
| def log_policy(self, checkpoint_dir: Path): |
| """Checkpoints the policy to wandb.""" |
| if self.cfg.disable_artifact: |
| return |
|
|
| step_id = checkpoint_dir.name |
| artifact_name = f"{self._group}-{step_id}" |
| artifact_name = get_safe_wandb_artifact_name(artifact_name) |
| artifact = self._wandb.Artifact(artifact_name, type="model") |
| pretrained_model_dir = checkpoint_dir / PRETRAINED_MODEL_DIR |
|
|
| |
| adapter_model_file = pretrained_model_dir / "adapter_model.safetensors" |
| standard_model_file = pretrained_model_dir / SAFETENSORS_SINGLE_FILE |
|
|
| if adapter_model_file.exists(): |
| |
| artifact.add_file(adapter_model_file) |
| adapter_config_file = pretrained_model_dir / "adapter_config.json" |
| if adapter_config_file.exists(): |
| artifact.add_file(adapter_config_file) |
| |
| config_file = pretrained_model_dir / "config.json" |
| if config_file.exists(): |
| artifact.add_file(config_file) |
| elif standard_model_file.exists(): |
| |
| artifact.add_file(standard_model_file) |
| else: |
| logging.warning( |
| f"No {SAFETENSORS_SINGLE_FILE} or adapter_model.safetensors found in {pretrained_model_dir}. " |
| "Skipping model artifact upload to WandB." |
| ) |
| return |
|
|
| self._wandb.log_artifact(artifact) |
|
|
| def log_dict( |
| self, d: dict, step: int | None = None, mode: str = "train", custom_step_key: str | None = None |
| ): |
| if mode not in {"train", "eval"}: |
| raise ValueError(mode) |
| if step is None and custom_step_key is None: |
| raise ValueError("Either step or custom_step_key must be provided.") |
|
|
| |
| |
| |
| |
| |
| if custom_step_key is not None: |
| if self._wandb_custom_step_key is None: |
| self._wandb_custom_step_key = set() |
| new_custom_key = f"{mode}/{custom_step_key}" |
| if new_custom_key not in self._wandb_custom_step_key: |
| self._wandb_custom_step_key.add(new_custom_key) |
| self._wandb.define_metric(new_custom_key, hidden=True) |
|
|
| batch_data = {} |
| for k, v in d.items(): |
| |
| if custom_step_key is not None and k == custom_step_key: |
| continue |
|
|
| if not isinstance(v, (int | float | str)): |
| logging.warning( |
| f'WandB logging of key "{k}" was ignored as its type "{type(v)}" is not handled by this wrapper.' |
| ) |
| continue |
|
|
| batch_data[f"{mode}/{k}"] = v |
|
|
| if batch_data: |
| if custom_step_key is not None: |
| batch_data[f"{mode}/{custom_step_key}"] = d[custom_step_key] |
| self._wandb.log(batch_data) |
| else: |
| self._wandb.log(data=batch_data, step=step) |
|
|
| def log_video(self, video_path: str, step: int, mode: str = "train", name: str = "video"): |
| if mode not in {"train", "eval"}: |
| raise ValueError(mode) |
|
|
| wandb_video = self._wandb.Video(video_path, fps=self.env_fps, format="mp4") |
| self._wandb.log({f"{mode}/{name}": wandb_video}, step=step) |
|
|