Files
2026-07-13 13:17:40 +08:00

197 lines
6.7 KiB
Python

import os
import tempfile
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional
import torch
from ray.air._internal.torch_utils import (
consume_prefix_in_state_dict_if_present_not_in_place,
load_torch_model,
)
from ray.train._internal.framework_checkpoint import FrameworkCheckpoint
from ray.util.annotations import PublicAPI
if TYPE_CHECKING:
from ray.data.preprocessor import Preprocessor
ENCODED_DATA_KEY = "torch_encoded_data"
@PublicAPI(stability="beta")
class TorchCheckpoint(FrameworkCheckpoint):
"""A :class:`~ray.train.Checkpoint` with Torch-specific functionality."""
MODEL_FILENAME = "model.pt"
@classmethod
def from_state_dict(
cls,
state_dict: Dict[str, Any],
*,
preprocessor: Optional["Preprocessor"] = None,
) -> "TorchCheckpoint":
"""Create a :class:`~ray.train.Checkpoint` that stores a model state dictionary.
.. tip::
This is the recommended method for creating
:class:`TorchCheckpoints<TorchCheckpoint>`.
Args:
state_dict: The model state dictionary to store in the checkpoint.
preprocessor: A fitted preprocessor to be applied before inference.
Returns:
A :class:`TorchCheckpoint` containing the specified state dictionary.
Examples:
.. testcode::
import torch
import torch.nn as nn
from ray.train.torch import TorchCheckpoint
# Set manual seed
torch.manual_seed(42)
# Function to create a NN model
def create_model() -> nn.Module:
model = nn.Sequential(nn.Linear(1, 10),
nn.ReLU(),
nn.Linear(10,1))
return model
# Create a TorchCheckpoint from our model's state_dict
model = create_model()
checkpoint = TorchCheckpoint.from_state_dict(model.state_dict())
# Now load the model from the TorchCheckpoint by providing the
# model architecture
model_from_chkpt = checkpoint.get_model(create_model())
# Assert they have the same state dict
assert str(model.state_dict()) == str(model_from_chkpt.state_dict())
print("worked")
.. testoutput::
:hide:
...
"""
tempdir = tempfile.mkdtemp()
model_path = Path(tempdir, cls.MODEL_FILENAME).as_posix()
stripped_state_dict = consume_prefix_in_state_dict_if_present_not_in_place(
state_dict, "module."
)
torch.save(stripped_state_dict, model_path)
checkpoint = cls.from_directory(tempdir)
if preprocessor:
checkpoint.set_preprocessor(preprocessor)
return checkpoint
@classmethod
def from_model(
cls,
model: torch.nn.Module,
*,
preprocessor: Optional["Preprocessor"] = None,
) -> "TorchCheckpoint":
"""Create a :class:`~ray.train.Checkpoint` that stores a Torch model.
.. note::
PyTorch recommends storing state dictionaries. To create a
:class:`TorchCheckpoint` from a state dictionary, call
:meth:`~ray.train.torch.TorchCheckpoint.from_state_dict`. To learn more
about state dictionaries, read
`Saving and Loading Models <https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict>`_. # noqa: E501
Args:
model: The Torch model to store in the checkpoint.
preprocessor: A fitted preprocessor to be applied before inference.
Returns:
A :class:`TorchCheckpoint` containing the specified model.
Examples:
.. testcode::
from ray.train.torch import TorchCheckpoint
import torch
# Create model identity and send a random tensor to it
model = torch.nn.Identity()
input = torch.randn(2, 2)
output = model(input)
# Create a checkpoint
checkpoint = TorchCheckpoint.from_model(model)
print(checkpoint)
.. testoutput::
:hide:
...
"""
tempdir = tempfile.mkdtemp()
model_path = Path(tempdir, cls.MODEL_FILENAME).as_posix()
torch.save(model, model_path)
checkpoint = cls.from_directory(tempdir)
if preprocessor:
checkpoint.set_preprocessor(preprocessor)
return checkpoint
def get_model(self, model: Optional[torch.nn.Module] = None) -> torch.nn.Module:
"""Retrieve the model stored in this checkpoint.
.. warning::
The checkpoint path must point to a **trusted** source.
Checkpoints created with
:meth:`~ray.train.torch.TorchCheckpoint.from_model` store the entire
``nn.Module`` via pickle serialization. Loading such a checkpoint from an
untrusted path (shared storage, downloaded artifact, checkpoint produced by
a different party) is equivalent to executing arbitrary Python code. Prefer
checkpoints created with
:meth:`~ray.train.torch.TorchCheckpoint.from_state_dict`, which stores
only model weights and is safe to load from untrusted sources.
Args:
model: If the checkpoint contains a model state dict, and not
the model itself, then the state dict will be loaded to this
``model``. Otherwise, the model will be discarded.
Returns:
The loaded ``torch.nn.Module``.
"""
with self.as_directory() as tempdir:
model_path = Path(tempdir, self.MODEL_FILENAME).as_posix()
if not os.path.exists(model_path):
raise RuntimeError(
"`model.pt` not found within this checkpoint. Make sure you "
"created this `TorchCheckpoint` from one of its public "
"constructors (`from_state_dict` or `from_model`)."
)
model_or_state_dict = torch.load(
model_path, map_location="cpu", weights_only=False
)
if isinstance(model_or_state_dict, torch.nn.Module):
if model:
warnings.warn(
"TorchCheckpoint already contains all information needed. "
"Discarding provided `model` argument."
)
model = load_torch_model(
saved_model=model_or_state_dict, model_definition=model
)
return model