Rate this Page

Source code for torchrl.render.checkpoint

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

import hashlib
import warnings
from collections.abc import Mapping
from pathlib import Path
from typing import Any

import torch
from torch import Tensor

from torchrl.checkpoint import Checkpoint, CheckpointFormat

__all__ = [
    "checkpoint_hash",
    "infer_state_dict",
    "load_checkpoint",
    "save_render_checkpoint",
]

_STATE_DICT_KEYS = ("model_state_dict", "policy", "state_dict")


class _StateDictComponent:
    def __init__(self, state_dict: Mapping[str, Any] | None = None) -> None:
        self.value = state_dict

    def state_dict(self) -> Mapping[str, Any]:
        if self.value is None:
            raise RuntimeError("No state dict has been assigned.")
        return self.value

    def load_state_dict(self, state_dict: Mapping[str, Any]) -> None:
        self.value = state_dict


[docs] def save_render_checkpoint( path: str | Path | None, model: Any, *, env_metadata: Mapping[str, Any] | None = None, frames: int | None = None, metrics: Mapping[str, Any] | None = None, config: Mapping[str, Any] | None = None, extra: Mapping[str, Any] | None = None, format: CheckpointFormat | None = None, ) -> Path | None: """Writes a checkpoint in the layout expected by rlrender factories. The model weights are stored under the canonical ``"model_state_dict"`` key probed by :func:`infer_state_dict`, and ``env_metadata`` entries are merged at the top level of the payload so environment and policy factories can rebuild the training setup. Conventional environment metadata keys used by the sota-implementations factories are ``"env_name"``, ``"env_backend"``, ``"env_config_overrides"``, ``"env_num_envs"``, ``"env_batch_mode"``, ``"normalize_observation"``, and ``"vecnorm"`` (frozen observation normalization statistics). Args: path: Destination checkpoint path. ``None`` or ``""`` disables checkpointing. model: Module exposing ``state_dict()``, or a ready state-dict mapping. env_metadata: Environment metadata merged into the payload. frames: Number of training frames collected so far. metrics: Scalar metrics recorded at checkpoint time. config: JSON-serializable training configuration. extra: Additional payload entries merged last. format: Unified checkpoint container format. When omitted, writes the legacy :func:`torch.save` payload during the compatibility window. Returns: The written checkpoint path, or ``None`` when checkpointing is disabled. Examples: >>> import tempfile >>> import torch >>> from torchrl.render import load_checkpoint, save_render_checkpoint >>> module = torch.nn.Linear(2, 2) >>> with tempfile.TemporaryDirectory() as tmpdir: ... path = save_render_checkpoint( ... f"{tmpdir}/policy.pt", ... module, ... env_metadata={"env_name": "CartPole-v1"}, ... format="archive", ... ) ... payload = load_checkpoint(path) >>> payload["env_name"] 'CartPole-v1' """ if path in (None, ""): return None path = Path(path).expanduser() if format is None: warnings.warn( "The default save_render_checkpoint output will change from the " "legacy torch.save payload to a unified TorchRL checkpoint in v0.15. " "Pass format='archive' or format='directory' to opt in now.", FutureWarning, stacklevel=2, ) payload: dict[str, Any] = { "model_state_dict": model if isinstance(model, Mapping) else model.state_dict() } if env_metadata: payload.update(dict(env_metadata)) if frames is not None: payload["frames"] = int(frames) if metrics is not None: payload["metrics"] = dict(metrics) if config is not None: payload["config"] = dict(config) if extra: payload.update(dict(extra)) path.parent.mkdir(parents=True, exist_ok=True) torch.save(payload, path) return path policy = _StateDictComponent(model) if isinstance(model, Mapping) else model components: dict[str, Any] = {"policy": policy} if env_metadata is not None: metadata = dict(env_metadata) components["environment_metadata"] = ( _StateDictComponent(metadata) if _contains_tensor(metadata) else metadata ) if frames is not None: components["trainer_state"] = {"frames": int(frames)} if metrics is not None: components["metrics"] = dict(metrics) if config is not None: components["config"] = dict(config) if extra is not None: extra_payload = dict(extra) components["extra"] = ( _StateDictComponent(extra_payload) if _contains_tensor(extra_payload) else extra_payload ) return Checkpoint(format=format, **components).save(path)
[docs] def load_checkpoint( path: str | Path, map_location: str | torch.device = "cpu", *, weights_only: bool | None = None, ) -> Any: """Loads a local PyTorch checkpoint. Args: path: Local checkpoint path. map_location: Device mapping passed to :func:`torch.load`. weights_only: Whether payloads are restricted to safe weight-only types. Unified checkpoints default to ``True``. Legacy payloads retain their historical ``False`` default for compatibility. Returns: The checkpoint payload. """ if "://" in str(path): raise ValueError( f"Only local checkpoint paths are supported in rlrender MVP, got {path!s}." ) path = Path(path).expanduser() if not path.exists(): raise FileNotFoundError(f"Checkpoint path does not exist: {path!s}.") try: if Checkpoint.is_checkpoint(path): return _load_unified_checkpoint( path, map_location, weights_only=True if weights_only is None else weights_only, ) return torch.load( path, map_location=map_location, weights_only=False if weights_only is None else weights_only, ) except Exception as err: raise RuntimeError( f"Could not load checkpoint {path!s}. If this checkpoint uses a custom " "format, provide a policy factory that loads it directly." ) from err
[docs] def checkpoint_hash(path: str | Path) -> str: """Compute a SHA256 digest over all checkpoint bytes.""" path = Path(path).expanduser() if not path.exists(): raise FileNotFoundError(f"Checkpoint path does not exist: {path!s}.") digest = hashlib.sha256() files = [path] if path.is_file() else sorted(path.rglob("*")) for file_path in files: if not file_path.is_file(): continue if path.is_dir(): digest.update(file_path.relative_to(path).as_posix().encode()) digest.update(b"\0") with file_path.open("rb") as file: for chunk in iter(lambda: file.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest()
def _load_unified_checkpoint( path: Path, map_location: str | torch.device, *, weights_only: bool, ) -> dict[str, Any]: manifest = Checkpoint.manifest(path) policy = _StateDictComponent() environment_metadata = _restore_target(manifest, "environment_metadata") trainer_state = _restore_target(manifest, "trainer_state") metrics = _restore_target(manifest, "metrics") config = _restore_target(manifest, "config") extra = _restore_target(manifest, "extra") checkpoint = Checkpoint( strict="ignore", policy=policy, environment_metadata=environment_metadata, trainer_state=trainer_state, metrics=metrics, config=config, extra=extra, ) requested = set(checkpoint.components).intersection(manifest["components"]) checkpoint.load( path, components=requested, map_location=map_location, tensor_load_kwargs={"weights_only": weights_only, "mmap": True}, strict="ignore", ) environment_metadata = _restored_mapping(environment_metadata) trainer_state = _restored_mapping(trainer_state) metrics = _restored_mapping(metrics) config = _restored_mapping(config) extra = _restored_mapping(extra) payload: dict[str, Any] = {} if policy.value is not None: payload["model_state_dict"] = policy.value payload.update(environment_metadata) frames = trainer_state.get("frames", trainer_state.get("collected_frames")) if frames is not None: payload["frames"] = frames if metrics: payload["metrics"] = metrics if config: payload["config"] = config payload.update(extra) return payload def _restore_target(manifest: Mapping[str, Any], name: str) -> Any: record = manifest["components"].get(name) if record is not None and record["adapter"] == "torchrl.state_dict": return _StateDictComponent() return {} def _restored_mapping(component: Any) -> dict[str, Any]: if isinstance(component, _StateDictComponent): return dict(component.value or {}) return component def _contains_tensor(value: Any) -> bool: if isinstance(value, Tensor): return True if isinstance(value, Mapping): return any(_contains_tensor(item) for item in value.values()) if isinstance(value, (list, tuple)): return any(_contains_tensor(item) for item in value) return False
[docs] def infer_state_dict(payload: Any, key: str | None = None) -> Mapping[str, Tensor]: """Infers a model state dict from common checkpoint payload layouts. Args: payload: Checkpoint payload. key: Explicit state-dict key to read from mapping payloads. Returns: A mapping from parameter names to tensors. """ if key is not None: if not isinstance(payload, Mapping) or key not in payload: raise KeyError( f"Checkpoint payload does not contain state-dict key {key!r}." ) payload = payload[key] elif isinstance(payload, Mapping): for candidate in _STATE_DICT_KEYS: value = payload.get(candidate) if _looks_like_state_dict(value): payload = value break if _looks_like_state_dict(payload): return payload raise TypeError( "Could not infer a tensor state dict from checkpoint payload. Expected one " "of 'model_state_dict', 'policy', 'state_dict', or a direct mapping of tensors." )
def _looks_like_state_dict(value: Any) -> bool: if not isinstance(value, Mapping) or not value: return False return all(isinstance(key, str) for key in value) and any( torch.is_tensor(item) for item in value.values() )