chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,954 @@
|
||||
import abc
|
||||
import copy
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
|
||||
|
||||
import pyarrow.fs
|
||||
|
||||
import ray
|
||||
import ray.cloudpickle as pickle
|
||||
from ray._common.usage import usage_lib
|
||||
from ray._private.dict import deep_update
|
||||
from ray.air._internal import usage as air_usage
|
||||
from ray.air._internal.config import ensure_only_allowed_dataclass_keys_updated
|
||||
from ray.air._internal.usage import AirEntrypoint
|
||||
from ray.air.config import RunConfig, ScalingConfig
|
||||
from ray.air.result import Result
|
||||
from ray.train import Checkpoint
|
||||
from ray.train._internal.session import get_session
|
||||
from ray.train._internal.storage import (
|
||||
StorageContext,
|
||||
_exists_at_fs_path,
|
||||
get_fs_and_path,
|
||||
)
|
||||
from ray.train.constants import (
|
||||
V2_MIGRATION_GUIDE_MESSAGE,
|
||||
_v2_migration_warnings_enabled,
|
||||
)
|
||||
from ray.train.context import _GET_METADATA_DEPRECATION_MESSAGE
|
||||
from ray.train.utils import _log_deprecation_warning
|
||||
from ray.util.annotations import Deprecated, DeveloperAPI, PublicAPI
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data import Dataset
|
||||
from ray.tune import Trainable
|
||||
|
||||
_TRAINER_PKL = "trainer.pkl"
|
||||
|
||||
# A type representing either a ray.data.Dataset or a function that returns a
|
||||
# ray.data.Dataset and accepts no arguments.
|
||||
GenDataset = Union["Dataset", Callable[[], "Dataset"]]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PREPROCESSOR_DEPRECATION_MESSAGE = (
|
||||
"The `preprocessor` argument to Trainers is deprecated as of Ray 2.7. "
|
||||
"Instead, use the Preprocessor `fit` and `transform` APIs directly on the Ray "
|
||||
"Dataset. For any state that needs to be saved to the trained checkpoint, pass it "
|
||||
"in using the `metadata` argument of the `Trainer`. "
|
||||
"For a full example, see "
|
||||
"https://docs.ray.io/en/master/train/user-guides/data-loading-preprocessing.html#preprocessing-structured-data " # noqa:E501
|
||||
)
|
||||
|
||||
_TRAINER_RESTORE_DEPRECATION_WARNING = (
|
||||
"The `restore` and `can_restore` APIs are deprecated and "
|
||||
f"will be removed in a future release. {V2_MIGRATION_GUIDE_MESSAGE}"
|
||||
)
|
||||
|
||||
_RESUME_FROM_CHECKPOINT_DEPRECATION_WARNING = (
|
||||
"`resume_from_checkpoint` is deprecated and will be removed in an upcoming "
|
||||
f"release. {V2_MIGRATION_GUIDE_MESSAGE}"
|
||||
)
|
||||
|
||||
|
||||
@PublicAPI(stability="beta")
|
||||
class TrainingFailedError(RuntimeError):
|
||||
"""An error indicating that training has failed."""
|
||||
|
||||
_RESTORE_MSG = (
|
||||
"The Ray Train run failed. Please inspect the previous error messages for a "
|
||||
"cause. After fixing the issue (assuming that the error is not caused by "
|
||||
"your own application logic, but rather an error such as OOM), you can restart "
|
||||
"the run from scratch or continue this run.\n"
|
||||
"To continue this run, you can use: "
|
||||
'`trainer = {trainer_cls_name}.restore("{path}")`.'
|
||||
)
|
||||
|
||||
_FAILURE_CONFIG_MSG = (
|
||||
"To start a new run that will retry on training failures, set "
|
||||
"`train.RunConfig(failure_config=train.FailureConfig(max_failures))` "
|
||||
"in the Trainer's `run_config` with `max_failures > 0`, or `max_failures = -1` "
|
||||
"for unlimited retries."
|
||||
)
|
||||
|
||||
|
||||
def _train_coordinator_fn(
|
||||
config: dict, trainer_cls: Type["BaseTrainer"], metadata: dict
|
||||
):
|
||||
"""This is the function that defines the logic of the Ray Train coordinator.
|
||||
This is responsible for setting up a remote instance of the `trainer_cls`
|
||||
(a different instance than the one calling `trainer.fit` on the driver!)
|
||||
and running the training loop.
|
||||
"""
|
||||
assert metadata is not None, metadata
|
||||
# Propagate user metadata from the Trainer constructor.
|
||||
get_session().metadata = metadata
|
||||
|
||||
# config already contains merged values.
|
||||
# Instantiate new Trainer in Trainable.
|
||||
trainer = trainer_cls(**config)
|
||||
|
||||
# Get the checkpoint from Tune and pass it to workers later on.
|
||||
checkpoint = ray.tune.get_checkpoint()
|
||||
if checkpoint:
|
||||
# Set `starting_checkpoint` for auto-recovery fault-tolerance
|
||||
# as well as manual restoration.
|
||||
trainer.starting_checkpoint = checkpoint
|
||||
# else: Train will restore from the user-provided
|
||||
# `resume_from_checkpoint` == `starting_checkpoint`.
|
||||
|
||||
# Evaluate datasets if they are wrapped in a factory.
|
||||
trainer.datasets = {
|
||||
k: d() if callable(d) else d for k, d in trainer.datasets.items()
|
||||
}
|
||||
|
||||
trainer.setup()
|
||||
trainer.training_loop()
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class BaseTrainer(abc.ABC):
|
||||
"""Defines interface for distributed training on Ray.
|
||||
|
||||
Note: The base ``BaseTrainer`` class cannot be instantiated directly. Only
|
||||
one of its subclasses can be used.
|
||||
|
||||
Note to developers: If a new trainer is added, please update
|
||||
`air/_internal/usage.py`.
|
||||
|
||||
**How does a trainer work?**
|
||||
|
||||
- First, initialize the Trainer. The initialization runs locally,
|
||||
so heavyweight setup should not be done in ``__init__``.
|
||||
- Then, when you call ``trainer.fit()``, the Trainer is serialized
|
||||
and copied to a remote Ray actor. The following methods are then
|
||||
called in sequence on the remote actor.
|
||||
- ``trainer.setup()``: Any heavyweight Trainer setup should be
|
||||
specified here.
|
||||
- ``trainer.training_loop()``: Executes the main training logic.
|
||||
- Calling ``trainer.fit()`` will return a ``ray.result.Result``
|
||||
object where you can access metrics from your training run, as well
|
||||
as any checkpoints that may have been saved.
|
||||
|
||||
**How do I create a new Trainer?**
|
||||
|
||||
Subclass ``ray.train.trainer.BaseTrainer``, and override the ``training_loop``
|
||||
method, and optionally ``setup``.
|
||||
|
||||
.. testcode::
|
||||
:skipif: True
|
||||
|
||||
import torch
|
||||
|
||||
from ray.train.trainer import BaseTrainer
|
||||
from ray import train, tune
|
||||
|
||||
|
||||
class MyPytorchTrainer(BaseTrainer):
|
||||
def setup(self):
|
||||
self.model = torch.nn.Linear(1, 1)
|
||||
self.optimizer = torch.optim.SGD(
|
||||
self.model.parameters(), lr=0.1)
|
||||
|
||||
def training_loop(self):
|
||||
# You can access any Trainer attributes directly in this method.
|
||||
# self.datasets["train"] has already been
|
||||
dataset = self.datasets["train"]
|
||||
|
||||
torch_ds = dataset.iter_torch_batches(dtypes=torch.float)
|
||||
loss_fn = torch.nn.MSELoss()
|
||||
|
||||
for epoch_idx in range(10):
|
||||
loss = 0
|
||||
num_batches = 0
|
||||
torch_ds = dataset.iter_torch_batches(
|
||||
dtypes=torch.float, batch_size=2
|
||||
)
|
||||
for batch in torch_ds:
|
||||
X = torch.unsqueeze(batch["x"], 1)
|
||||
y = torch.unsqueeze(batch["y"], 1)
|
||||
# Compute prediction error
|
||||
pred = self.model(X)
|
||||
batch_loss = loss_fn(pred, y)
|
||||
|
||||
# Backpropagation
|
||||
self.optimizer.zero_grad()
|
||||
batch_loss.backward()
|
||||
self.optimizer.step()
|
||||
|
||||
loss += batch_loss.item()
|
||||
num_batches += 1
|
||||
loss /= num_batches
|
||||
|
||||
# Use Tune functions to report intermediate
|
||||
# results.
|
||||
train.report({"loss": loss, "epoch": epoch_idx})
|
||||
|
||||
|
||||
# Initialize the Trainer, and call Trainer.fit()
|
||||
import ray
|
||||
train_dataset = ray.data.from_items(
|
||||
[{"x": i, "y": i} for i in range(10)])
|
||||
my_trainer = MyPytorchTrainer(datasets={"train": train_dataset})
|
||||
result = my_trainer.fit()
|
||||
|
||||
Args:
|
||||
scaling_config: Configuration for how to scale training.
|
||||
run_config: Configuration for the execution of the training run.
|
||||
datasets: Any Datasets to use for training. Use the key "train"
|
||||
to denote which dataset is the training dataset.
|
||||
metadata: Dict that should be made available via
|
||||
`train.get_context().get_metadata()` and in `checkpoint.get_metadata()`
|
||||
for checkpoints saved from this Trainer. Must be JSON-serializable.
|
||||
resume_from_checkpoint: A checkpoint to resume training from.
|
||||
"""
|
||||
|
||||
_scaling_config_allowed_keys: List[str] = [
|
||||
"trainer_resources",
|
||||
]
|
||||
_handles_checkpoint_freq: bool = False
|
||||
_handles_checkpoint_at_end: bool = False
|
||||
|
||||
# fields to propagate to Tuner param_space.
|
||||
# See `BaseTrainer._extract_fields_for_tuner_param_space` for more details.
|
||||
_fields_for_tuner_param_space = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scaling_config: Optional[ScalingConfig] = None,
|
||||
run_config: Optional[RunConfig] = None,
|
||||
datasets: Optional[Dict[str, GenDataset]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
resume_from_checkpoint: Optional[Checkpoint] = None,
|
||||
):
|
||||
self.scaling_config = (
|
||||
scaling_config if scaling_config is not None else ScalingConfig()
|
||||
)
|
||||
self.run_config = (
|
||||
copy.copy(run_config) if run_config is not None else RunConfig()
|
||||
)
|
||||
self.metadata = metadata
|
||||
self.datasets = datasets if datasets is not None else {}
|
||||
self.starting_checkpoint = resume_from_checkpoint
|
||||
|
||||
if _v2_migration_warnings_enabled():
|
||||
if metadata is not None:
|
||||
_log_deprecation_warning(_GET_METADATA_DEPRECATION_MESSAGE)
|
||||
if resume_from_checkpoint is not None:
|
||||
_log_deprecation_warning(_RESUME_FROM_CHECKPOINT_DEPRECATION_WARNING)
|
||||
|
||||
# These attributes should only be set through `BaseTrainer.restore`
|
||||
self._restore_path = None
|
||||
self._restore_storage_filesystem = None
|
||||
|
||||
self._validate_attributes()
|
||||
|
||||
usage_lib.record_library_usage("train")
|
||||
air_usage.tag_air_trainer(self)
|
||||
|
||||
@classmethod
|
||||
@Deprecated(message=_TRAINER_RESTORE_DEPRECATION_WARNING)
|
||||
def restore(
|
||||
cls: Type["BaseTrainer"],
|
||||
path: Union[str, os.PathLike],
|
||||
storage_filesystem: Optional[pyarrow.fs.FileSystem] = None,
|
||||
datasets: Optional[Dict[str, GenDataset]] = None,
|
||||
scaling_config: Optional[ScalingConfig] = None,
|
||||
**kwargs,
|
||||
) -> "BaseTrainer":
|
||||
"""Restores a Train experiment from a previously interrupted/failed run.
|
||||
|
||||
Restore should be used for experiment-level fault tolerance in the event
|
||||
that the head node crashes (e.g., OOM or some other runtime error) or the
|
||||
entire cluster goes down (e.g., network error affecting all nodes).
|
||||
|
||||
A run that has already completed successfully will not be resumed from this API.
|
||||
To continue training from a successful run, launch a new run with the
|
||||
``<Framework>Trainer(resume_from_checkpoint)`` API instead, passing in a
|
||||
checkpoint from the previous run to start with.
|
||||
|
||||
.. warning::
|
||||
|
||||
The ``path`` must point to a **trusted** experiment directory.
|
||||
Restoring from an untrusted path executes arbitrary Python code
|
||||
(the experiment state uses pickle serialization). Never restore
|
||||
from a path that other parties can write to.
|
||||
|
||||
.. note::
|
||||
|
||||
Restoring an experiment from a path that's pointing to a *different*
|
||||
location than the original experiment path is supported. However, Ray Train
|
||||
assumes that the full experiment directory is available
|
||||
(including checkpoints) so that it's possible to resume trials from their
|
||||
latest state.
|
||||
|
||||
For example, if the original experiment path was run locally, then the
|
||||
results are uploaded to cloud storage, Ray Train expects the full contents
|
||||
to be available in cloud storage if attempting to resume
|
||||
via ``<Framework>Trainer.restore("s3://...")``. The restored run will
|
||||
continue writing results to the same cloud storage location.
|
||||
|
||||
The following example can be paired with implementing job retry using
|
||||
:ref:`Ray Jobs <jobs-overview>` to produce a Train experiment that will
|
||||
attempt to resume on both experiment-level and trial-level failures:
|
||||
|
||||
.. testcode::
|
||||
:skipif: True
|
||||
|
||||
import os
|
||||
import ray
|
||||
from ray import train
|
||||
from ray.train.trainer import BaseTrainer
|
||||
|
||||
experiment_name = "unique_experiment_name"
|
||||
storage_path = os.path.expanduser("~/ray_results")
|
||||
experiment_dir = os.path.join(storage_path, experiment_name)
|
||||
|
||||
# Define some dummy inputs for demonstration purposes
|
||||
datasets = {"train": ray.data.from_items([{"a": i} for i in range(10)])}
|
||||
|
||||
class CustomTrainer(BaseTrainer):
|
||||
def training_loop(self):
|
||||
pass
|
||||
|
||||
if CustomTrainer.can_restore(experiment_dir):
|
||||
trainer = CustomTrainer.restore(
|
||||
experiment_dir, datasets=datasets
|
||||
)
|
||||
else:
|
||||
trainer = CustomTrainer(
|
||||
datasets=datasets,
|
||||
run_config=train.RunConfig(
|
||||
name=experiment_name,
|
||||
storage_path=storage_path,
|
||||
# Tip: You can also enable retries on failure for
|
||||
# worker-level fault tolerance
|
||||
failure_config=train.FailureConfig(max_failures=3),
|
||||
),
|
||||
)
|
||||
|
||||
result = trainer.fit()
|
||||
|
||||
Args:
|
||||
path: The path to the experiment directory of the training run to restore.
|
||||
This can be a local path or a remote URI if the experiment was
|
||||
uploaded to the cloud.
|
||||
storage_filesystem: Custom ``pyarrow.fs.FileSystem``
|
||||
corresponding to the ``path``. This may be necessary if the original
|
||||
experiment passed in a custom filesystem.
|
||||
datasets: Re-specified datasets used in the original training run.
|
||||
This must include all the datasets that were passed in the
|
||||
original trainer constructor.
|
||||
scaling_config: Optionally re-specified scaling config. This can be
|
||||
modified to be different from the original spec.
|
||||
**kwargs: Other optionally re-specified arguments, passed in by subclasses.
|
||||
|
||||
Raises:
|
||||
ValueError: If all datasets were not re-supplied on restore.
|
||||
|
||||
Returns:
|
||||
BaseTrainer: A restored instance of the class that is calling this method.
|
||||
"""
|
||||
if _v2_migration_warnings_enabled():
|
||||
_log_deprecation_warning(_TRAINER_RESTORE_DEPRECATION_WARNING)
|
||||
|
||||
if not cls.can_restore(path, storage_filesystem):
|
||||
raise ValueError(
|
||||
f"Invalid restore path: {path}. Make sure that this path exists and "
|
||||
"is the experiment directory that results from a call to "
|
||||
"`trainer.fit()`."
|
||||
)
|
||||
fs, fs_path = get_fs_and_path(path, storage_filesystem)
|
||||
trainer_pkl_path = Path(fs_path, _TRAINER_PKL).as_posix()
|
||||
with fs.open_input_file(trainer_pkl_path) as f:
|
||||
trainer_cls, param_dict = pickle.loads(f.readall())
|
||||
|
||||
if trainer_cls is not cls:
|
||||
warnings.warn(
|
||||
f"Invalid trainer type. You are attempting to restore a trainer of type"
|
||||
f" {trainer_cls} with `{cls.__name__}.restore`, "
|
||||
"which will most likely fail. "
|
||||
f"Use `{trainer_cls.__name__}.restore` instead."
|
||||
)
|
||||
|
||||
original_datasets = param_dict.pop("datasets", {})
|
||||
if original_datasets and not datasets:
|
||||
raise ValueError(
|
||||
"The following datasets need to be provided again on restore: "
|
||||
f"{list(original_datasets.keys())}\n"
|
||||
f"Use {cls.__name__}.restore(..., datasets=datasets) "
|
||||
"with the datasets that were provided to the original trainer."
|
||||
)
|
||||
datasets = datasets or {}
|
||||
if set(original_datasets) != set(datasets):
|
||||
raise ValueError(
|
||||
"The provided datasets don't match the original dataset keys.\n"
|
||||
f" Expected datasets for the keys: {list(original_datasets.keys())}\n"
|
||||
f" Actual datasets provided: {list(datasets.keys())}"
|
||||
)
|
||||
param_dict["datasets"] = datasets
|
||||
|
||||
if scaling_config:
|
||||
param_dict["scaling_config"] = scaling_config
|
||||
|
||||
for param_name, val in kwargs.items():
|
||||
# Overwrite the old value if something is passed into restore
|
||||
if val is not None:
|
||||
param_dict[param_name] = val
|
||||
|
||||
try:
|
||||
trainer = cls(**param_dict)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
"Trainer restoration failed (see above for the stack trace). "
|
||||
"Make sure that you use the right trainer class to restore: "
|
||||
f"`{cls.__name__}.restore`\n"
|
||||
) from e
|
||||
trainer._restore_path = path
|
||||
trainer._restore_storage_filesystem = storage_filesystem
|
||||
return trainer
|
||||
|
||||
@classmethod
|
||||
@Deprecated(
|
||||
message=_TRAINER_RESTORE_DEPRECATION_WARNING,
|
||||
warning=_v2_migration_warnings_enabled(),
|
||||
)
|
||||
def can_restore(
|
||||
cls: Type["BaseTrainer"],
|
||||
path: Union[str, os.PathLike],
|
||||
storage_filesystem: Optional[pyarrow.fs.FileSystem] = None,
|
||||
) -> bool:
|
||||
"""Checks whether a given directory contains a restorable Train experiment.
|
||||
|
||||
Args:
|
||||
path: The path to the experiment directory of the Train experiment.
|
||||
This can be either a local directory (e.g., ~/ray_results/exp_name)
|
||||
or a remote URI (e.g., s3://bucket/exp_name).
|
||||
storage_filesystem: Custom ``pyarrow.fs.FileSystem`` to use. If not
|
||||
provided, the filesystem is auto-resolved from ``path``.
|
||||
|
||||
Returns:
|
||||
bool: Whether this path exists and contains the trainer state to resume from
|
||||
"""
|
||||
if _v2_migration_warnings_enabled():
|
||||
_log_deprecation_warning(_TRAINER_RESTORE_DEPRECATION_WARNING)
|
||||
|
||||
fs, fs_path = get_fs_and_path(path, storage_filesystem)
|
||||
trainer_pkl_path = Path(fs_path, _TRAINER_PKL).as_posix()
|
||||
return _exists_at_fs_path(fs, trainer_pkl_path)
|
||||
|
||||
def __repr__(self):
|
||||
# A dictionary that maps parameters to their default values.
|
||||
default_values: Dict[str, Any] = {
|
||||
"scaling_config": ScalingConfig(),
|
||||
"run_config": RunConfig(),
|
||||
"datasets": {},
|
||||
"starting_checkpoint": None,
|
||||
}
|
||||
|
||||
non_default_arguments = []
|
||||
for parameter, default_value in default_values.items():
|
||||
value = getattr(self, parameter)
|
||||
if value != default_value:
|
||||
# 'Dataset.__repr__' returns a table rather than a regular Python object
|
||||
# representation. So, we need to special case the 'datasets' parameter.
|
||||
if parameter == "datasets":
|
||||
value_repr = format_datasets_for_repr(value)
|
||||
else:
|
||||
value_repr = repr(value)
|
||||
|
||||
non_default_arguments.append(f"{parameter}={value_repr}")
|
||||
|
||||
if non_default_arguments:
|
||||
return f"<{self.__class__.__name__} {' '.join(non_default_arguments)}>"
|
||||
|
||||
return f"<{self.__class__.__name__}>"
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# Store the init args as attributes so this can be merged with Tune hparams.
|
||||
trainer = super(BaseTrainer, cls).__new__(cls)
|
||||
parameters = inspect.signature(cls.__init__).parameters
|
||||
parameters = list(parameters.keys())
|
||||
# Remove self.
|
||||
parameters = parameters[1:]
|
||||
arg_dict = dict(zip(parameters, args))
|
||||
trainer._param_dict = {**arg_dict, **kwargs}
|
||||
return trainer
|
||||
|
||||
def _validate_attributes(self):
|
||||
"""Called on __init()__ to validate trainer attributes."""
|
||||
# Run config
|
||||
if not isinstance(self.run_config, RunConfig):
|
||||
raise ValueError(
|
||||
f"`run_config` should be an instance of `ray.train.RunConfig`, "
|
||||
f"found {type(self.run_config)} with value `{self.run_config}`."
|
||||
)
|
||||
# Scaling config
|
||||
if not isinstance(self.scaling_config, ScalingConfig):
|
||||
raise ValueError(
|
||||
"`scaling_config` should be an instance of `ScalingConfig`, "
|
||||
f"found {type(self.scaling_config)} with value `{self.scaling_config}`."
|
||||
)
|
||||
# Datasets
|
||||
if not isinstance(self.datasets, dict):
|
||||
raise ValueError(
|
||||
f"`datasets` should be a dict mapping from a string to "
|
||||
f"`ray.data.Dataset` objects, "
|
||||
f"found {type(self.datasets)} with value `{self.datasets}`."
|
||||
)
|
||||
else:
|
||||
for key, dataset in self.datasets.items():
|
||||
if not isinstance(dataset, ray.data.Dataset) and not callable(dataset):
|
||||
raise ValueError(
|
||||
f"The Dataset under '{key}' key is not a "
|
||||
"`ray.data.Dataset`. "
|
||||
f"Received {dataset} instead."
|
||||
)
|
||||
# Metadata.
|
||||
self.metadata = self.metadata or {}
|
||||
if not isinstance(self.metadata, dict):
|
||||
raise TypeError(
|
||||
f"The provided metadata must be a dict, was {type(self.metadata)}."
|
||||
)
|
||||
try:
|
||||
self.metadata = json.loads(json.dumps(self.metadata))
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
"The provided metadata must be JSON-serializable: "
|
||||
f"{self.metadata}: {e}"
|
||||
)
|
||||
|
||||
if self.starting_checkpoint is not None and not isinstance(
|
||||
self.starting_checkpoint, Checkpoint
|
||||
):
|
||||
raise ValueError(
|
||||
f"`resume_from_checkpoint` should be an instance of "
|
||||
f"`ray.train.Checkpoint`, found {type(self.starting_checkpoint)} "
|
||||
f"with value `{self.starting_checkpoint}`."
|
||||
)
|
||||
|
||||
self._log_v2_deprecation_warnings()
|
||||
|
||||
def _log_v2_deprecation_warnings(self):
|
||||
"""Logs deprecation warnings for v2 migration.
|
||||
|
||||
Log them here in the Ray Train case rather than in the configuration
|
||||
constructors to avoid logging incorrect deprecation warnings when
|
||||
`ray.train.RunConfig` is passed to Ray Tune.
|
||||
"""
|
||||
from ray.train.v2._internal.constants import V2_ENABLED_ENV_VAR, is_v2_enabled
|
||||
|
||||
if is_v2_enabled():
|
||||
raise DeprecationWarning(
|
||||
f"Detected use of a deprecated Trainer import from `{self.__class__.__module__}`. "
|
||||
"This Trainer class is not compatible with Ray Train V2.\n"
|
||||
"To fix this:\n"
|
||||
" - Update to use the new import path. For example, "
|
||||
"`from ray.train.torch.torch_trainer import TorchTrainer` -> "
|
||||
"`from ray.train.torch import TorchTrainer`\n"
|
||||
f" - Or, explicitly disable V2 by setting: {V2_ENABLED_ENV_VAR}=0\n"
|
||||
"See this issue for more context: "
|
||||
"https://github.com/ray-project/ray/issues/49454"
|
||||
)
|
||||
|
||||
if not _v2_migration_warnings_enabled():
|
||||
return
|
||||
|
||||
from ray.train.v2._internal.migration_utils import (
|
||||
CALLBACKS_DEPRECATION_MESSAGE,
|
||||
FAIL_FAST_DEPRECATION_MESSAGE,
|
||||
LOG_TO_FILE_DEPRECATION_MESSAGE,
|
||||
PROGRESS_REPORTER_DEPRECATION_MESSAGE,
|
||||
STOP_DEPRECATION_MESSAGE,
|
||||
SYNC_CONFIG_DEPRECATION_MESSAGE,
|
||||
TRAINER_RESOURCES_DEPRECATION_MESSAGE,
|
||||
VERBOSE_DEPRECATION_MESSAGE,
|
||||
)
|
||||
|
||||
# ScalingConfig deprecations
|
||||
if self.scaling_config.trainer_resources is not None:
|
||||
_log_deprecation_warning(TRAINER_RESOURCES_DEPRECATION_MESSAGE)
|
||||
|
||||
# FailureConfig deprecations
|
||||
if self.run_config.failure_config.fail_fast:
|
||||
_log_deprecation_warning(FAIL_FAST_DEPRECATION_MESSAGE)
|
||||
|
||||
# RunConfig deprecations
|
||||
# NOTE: _verbose is the original verbose value passed by the user
|
||||
if self.run_config._verbose is not None:
|
||||
_log_deprecation_warning(VERBOSE_DEPRECATION_MESSAGE)
|
||||
|
||||
if self.run_config.log_to_file:
|
||||
_log_deprecation_warning(LOG_TO_FILE_DEPRECATION_MESSAGE)
|
||||
|
||||
if self.run_config.stop is not None:
|
||||
_log_deprecation_warning(STOP_DEPRECATION_MESSAGE)
|
||||
|
||||
if self.run_config.callbacks is not None:
|
||||
_log_deprecation_warning(CALLBACKS_DEPRECATION_MESSAGE)
|
||||
|
||||
if self.run_config.progress_reporter is not None:
|
||||
_log_deprecation_warning(PROGRESS_REPORTER_DEPRECATION_MESSAGE)
|
||||
|
||||
if self.run_config.sync_config != ray.train.SyncConfig():
|
||||
_log_deprecation_warning(SYNC_CONFIG_DEPRECATION_MESSAGE)
|
||||
|
||||
@classmethod
|
||||
def _validate_scaling_config(cls, scaling_config: ScalingConfig) -> ScalingConfig:
|
||||
"""Returns scaling config dataclass after validating updated keys."""
|
||||
ensure_only_allowed_dataclass_keys_updated(
|
||||
dataclass=scaling_config,
|
||||
allowed_keys=cls._scaling_config_allowed_keys,
|
||||
)
|
||||
return scaling_config
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Called during fit() to perform initial setup on the Trainer.
|
||||
|
||||
.. note:: This method is run on a remote process.
|
||||
|
||||
This method will not be called on the driver, so any expensive setup
|
||||
operations should be placed here and not in ``__init__``.
|
||||
|
||||
This method is called prior to ``preprocess_datasets`` and
|
||||
``training_loop``.
|
||||
"""
|
||||
pass
|
||||
|
||||
def preprocess_datasets(self) -> None:
|
||||
"""Deprecated."""
|
||||
raise DeprecationWarning(
|
||||
"`preprocess_datasets` is no longer used, since preprocessors "
|
||||
f"are no longer accepted by Trainers.\n{PREPROCESSOR_DEPRECATION_MESSAGE}"
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def training_loop(self) -> None:
|
||||
"""Loop called by fit() to run training and report results to Tune.
|
||||
|
||||
.. note:: This method runs on a remote process.
|
||||
|
||||
``self.datasets`` have already been evaluated if they were wrapped in a factory.
|
||||
|
||||
You can use the :ref:`Ray Train utilities <train-loop-api>`
|
||||
(:func:`train.report() <ray.train.report>` and
|
||||
:func:`train.get_checkpoint() <ray.train.get_checkpoint>`) inside
|
||||
this training loop.
|
||||
|
||||
Example:
|
||||
|
||||
.. testcode::
|
||||
|
||||
from ray.train.trainer import BaseTrainer
|
||||
from ray import train
|
||||
|
||||
class MyTrainer(BaseTrainer):
|
||||
def training_loop(self):
|
||||
for epoch_idx in range(5):
|
||||
...
|
||||
train.report({"epoch": epoch_idx})
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@PublicAPI(stability="beta")
|
||||
def fit(self) -> Result:
|
||||
"""Runs training.
|
||||
|
||||
Returns:
|
||||
A Result object containing the training result.
|
||||
|
||||
Raises:
|
||||
ray.train.base_trainer.TrainingFailedError: If any failures during the execution
|
||||
of ``self.as_trainable()``, or during the Tune execution loop.
|
||||
"""
|
||||
from ray.tune import ResumeConfig, TuneError
|
||||
from ray.tune.tuner import Tuner
|
||||
|
||||
trainable = self.as_trainable()
|
||||
param_space = self._extract_fields_for_tuner_param_space()
|
||||
|
||||
self.run_config.name = (
|
||||
self.run_config.name or StorageContext.get_experiment_dir_name(trainable)
|
||||
)
|
||||
# The storage context here is only used to access the resolved
|
||||
# storage fs and experiment path, in order to avoid duplicating that logic.
|
||||
# This is NOT the storage context object that gets passed to remote workers.
|
||||
storage = StorageContext(
|
||||
storage_path=self.run_config.storage_path,
|
||||
experiment_dir_name=self.run_config.name,
|
||||
storage_filesystem=self.run_config.storage_filesystem,
|
||||
)
|
||||
|
||||
if self._restore_path:
|
||||
tuner = Tuner.restore(
|
||||
path=self._restore_path,
|
||||
trainable=trainable,
|
||||
param_space=param_space,
|
||||
_resume_config=ResumeConfig(
|
||||
finished=ResumeConfig.ResumeType.RESUME,
|
||||
unfinished=ResumeConfig.ResumeType.RESUME,
|
||||
errored=ResumeConfig.ResumeType.RESUME,
|
||||
),
|
||||
storage_filesystem=self._restore_storage_filesystem,
|
||||
)
|
||||
else:
|
||||
tuner = Tuner(
|
||||
trainable=trainable,
|
||||
param_space=param_space,
|
||||
run_config=self.run_config,
|
||||
_entrypoint=AirEntrypoint.TRAINER,
|
||||
)
|
||||
|
||||
self._save(storage.storage_filesystem, storage.experiment_fs_path)
|
||||
|
||||
restore_msg = TrainingFailedError._RESTORE_MSG.format(
|
||||
trainer_cls_name=self.__class__.__name__,
|
||||
path=str(storage.experiment_fs_path),
|
||||
)
|
||||
|
||||
try:
|
||||
result_grid = tuner.fit()
|
||||
except TuneError as e:
|
||||
# Catch any `TuneError`s raised by the `Tuner.fit` call.
|
||||
# Unwrap the `TuneError` if needed.
|
||||
parent_error = e.__cause__ or e
|
||||
|
||||
# Raise it to the user as a `TrainingFailedError` with a message to restore.
|
||||
raise TrainingFailedError(restore_msg) from parent_error
|
||||
# Other exceptions get passed through directly (ex: on `fail_fast='raise'`)
|
||||
|
||||
assert len(result_grid) == 1
|
||||
result = result_grid[0]
|
||||
if result.error:
|
||||
# Raise trainable errors to the user with a message to restore
|
||||
# or configure `FailureConfig` in a new run.
|
||||
raise TrainingFailedError(
|
||||
"\n".join([restore_msg, TrainingFailedError._FAILURE_CONFIG_MSG])
|
||||
) from result.error
|
||||
return result
|
||||
|
||||
def _save(self, fs: pyarrow.fs.FileSystem, experiment_path: str):
|
||||
"""Saves the current trainer's class along with the `param_dict` of
|
||||
parameters passed to this trainer's constructor.
|
||||
|
||||
This is used to recreate the trainer on restore.
|
||||
Unless a parameter is re-specified during restoration (only a subset
|
||||
of parameters can be passed in again), that parameter will be loaded
|
||||
from the saved copy.
|
||||
|
||||
Datasets should not be saved as part of the state. Instead, we save the
|
||||
keys and replace the dataset values with dummy functions that will
|
||||
raise an error if invoked. The error only serves as a guardrail for
|
||||
misuse (e.g., manually unpickling and constructing the Trainer again)
|
||||
and is not typically surfaced, since datasets must be re-specified
|
||||
upon restoration.
|
||||
"""
|
||||
param_dict = self._param_dict.copy()
|
||||
datasets = param_dict.pop("datasets", {})
|
||||
|
||||
def raise_fn():
|
||||
raise RuntimeError
|
||||
|
||||
if datasets:
|
||||
param_dict["datasets"] = dict.fromkeys(datasets, raise_fn)
|
||||
|
||||
cls_and_param_dict = (self.__class__, param_dict)
|
||||
|
||||
fs.create_dir(experiment_path)
|
||||
with fs.open_output_stream(Path(experiment_path, _TRAINER_PKL).as_posix()) as f:
|
||||
f.write(pickle.dumps(cls_and_param_dict))
|
||||
|
||||
def _extract_fields_for_tuner_param_space(self) -> Dict:
|
||||
"""Extracts fields to be included in `Tuner.param_space`.
|
||||
|
||||
This is needed to leverage the full logging/integration offerings from Tune.
|
||||
For example, `param_space` is logged automatically to wandb integration.
|
||||
|
||||
Currently only done for `train_loop_config`.
|
||||
|
||||
Returns:
|
||||
A dictionary that should be passed to Tuner.param_space.
|
||||
"""
|
||||
result = {}
|
||||
for key in self._fields_for_tuner_param_space:
|
||||
if key in self._param_dict.keys():
|
||||
result[key] = copy.deepcopy(self._param_dict[key])
|
||||
return result
|
||||
|
||||
def _generate_trainable_cls(self) -> Type["Trainable"]:
|
||||
"""Generates the base Trainable class.
|
||||
|
||||
Returns:
|
||||
A Trainable class to use for training.
|
||||
"""
|
||||
|
||||
from ray.tune.execution.placement_groups import PlacementGroupFactory
|
||||
from ray.tune.trainable import wrap_function
|
||||
|
||||
trainer_cls = self.__class__
|
||||
scaling_config = self.scaling_config
|
||||
metadata = self.metadata
|
||||
|
||||
train_coordinator_fn = partial(
|
||||
_train_coordinator_fn, trainer_cls=trainer_cls, metadata=metadata
|
||||
)
|
||||
# Change the name of the training function to match the name of the Trainer
|
||||
# class. This will mean the Tune trial name will match the name of Trainer on
|
||||
# stdout messages and the results directory.
|
||||
train_coordinator_fn.__name__ = trainer_cls.__name__
|
||||
|
||||
trainable_cls = wrap_function(train_coordinator_fn)
|
||||
has_base_dataset = bool(self.datasets)
|
||||
if has_base_dataset:
|
||||
from ray.data.context import DataContext
|
||||
|
||||
dataset_context = DataContext.get_current()
|
||||
else:
|
||||
dataset_context = None
|
||||
|
||||
class TrainTrainable(trainable_cls):
|
||||
"""Adds default resources to the Trainable."""
|
||||
|
||||
_handles_checkpoint_freq = trainer_cls._handles_checkpoint_freq
|
||||
_handles_checkpoint_at_end = trainer_cls._handles_checkpoint_at_end
|
||||
|
||||
@classmethod
|
||||
def has_base_dataset(cls) -> bool:
|
||||
"""Whether a dataset is provided through the Trainer."""
|
||||
return has_base_dataset
|
||||
|
||||
@classmethod
|
||||
def base_scaling_config(cls) -> ScalingConfig:
|
||||
"""Returns the unchanged scaling config provided through the Trainer."""
|
||||
return scaling_config
|
||||
|
||||
def setup(self, config, **kwargs):
|
||||
base_config = dict(kwargs)
|
||||
# Merge Tuner param space hyperparameters in `config` into the
|
||||
# base config passed to the Trainer constructor, which is `base_config`.
|
||||
# `base_config` is pulled from the object store from the usage of
|
||||
# tune.with_parameters in `BaseTrainer.as_trainable`.
|
||||
|
||||
# run_config is not a tunable hyperparameter so it does not need to be
|
||||
# merged.
|
||||
run_config = base_config.pop("run_config", None)
|
||||
self._merged_config = deep_update(
|
||||
base_config, self.config, new_keys_allowed=True
|
||||
)
|
||||
self._merged_config["run_config"] = run_config
|
||||
merged_scaling_config = self._merged_config.get(
|
||||
"scaling_config", ScalingConfig()
|
||||
)
|
||||
if isinstance(merged_scaling_config, dict):
|
||||
merged_scaling_config = ScalingConfig(**merged_scaling_config)
|
||||
self._merged_config[
|
||||
"scaling_config"
|
||||
] = self._reconcile_scaling_config_with_trial_resources(
|
||||
merged_scaling_config
|
||||
)
|
||||
if self.has_base_dataset():
|
||||
# Set the DataContext on the Trainer actor to the DataContext
|
||||
# specified on the driver.
|
||||
DataContext._set_current(dataset_context)
|
||||
super(TrainTrainable, self).setup(config)
|
||||
|
||||
def _reconcile_scaling_config_with_trial_resources(
|
||||
self, scaling_config: ScalingConfig
|
||||
) -> ScalingConfig:
|
||||
"""
|
||||
ResourceChangingScheduler workaround.
|
||||
|
||||
Ensures that the scaling config matches trial resources.
|
||||
|
||||
This should be replaced with RCS returning a ScalingConfig
|
||||
in the future.
|
||||
"""
|
||||
|
||||
trial_resources = self.trial_resources
|
||||
# This will be false if the resources are default
|
||||
if not isinstance(trial_resources, PlacementGroupFactory):
|
||||
return scaling_config
|
||||
|
||||
# Ignore ResourceChangingScheduler workaround when resource bundles
|
||||
# are unchanged
|
||||
if self.trial_resources == scaling_config.as_placement_group_factory():
|
||||
return scaling_config
|
||||
|
||||
trainer_cls._validate_scaling_config(scaling_config)
|
||||
|
||||
return ScalingConfig.from_placement_group_factory(trial_resources)
|
||||
|
||||
def _trainable_func(self, config):
|
||||
# We ignore the config passed by Tune and instead use the merged
|
||||
# config which includes the initial Trainer args.
|
||||
super()._trainable_func(self._merged_config)
|
||||
|
||||
@classmethod
|
||||
def default_resource_request(cls, config):
|
||||
# `config["scaling_config"] is a dataclass when passed via the
|
||||
# `scaling_config` argument in `Trainer` and is a dict when passed
|
||||
# via the `scaling_config` key of `param_spec`.
|
||||
|
||||
# Conversion logic must be duplicated in `TrainTrainable.__init__`
|
||||
# because this is a class method.
|
||||
updated_scaling_config = config.get("scaling_config", scaling_config)
|
||||
if isinstance(updated_scaling_config, dict):
|
||||
updated_scaling_config = ScalingConfig(**updated_scaling_config)
|
||||
validated_scaling_config = trainer_cls._validate_scaling_config(
|
||||
updated_scaling_config
|
||||
)
|
||||
return validated_scaling_config.as_placement_group_factory()
|
||||
|
||||
return TrainTrainable
|
||||
|
||||
def as_trainable(self) -> Type["Trainable"]:
|
||||
"""Converts self to a ``tune.Trainable`` class."""
|
||||
from ray import tune
|
||||
|
||||
base_config = self._param_dict
|
||||
trainable_cls = self._generate_trainable_cls()
|
||||
|
||||
# Wrap with `tune.with_parameters` to handle very large values in base_config
|
||||
return tune.with_parameters(trainable_cls, **base_config)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
def format_datasets_for_repr(datasets: Optional[Dict[str, GenDataset]]) -> str:
|
||||
"""Format datasets for BaseTrainer repr using plan strings.
|
||||
|
||||
The Dataset.__repr__ returns a table rather than a conventional Python object
|
||||
reprentation. To ensure the BaseTrainer representation still looks reasonable, we
|
||||
need to special-case datasets.
|
||||
"""
|
||||
from ray.data import Dataset
|
||||
from ray.data._internal.dataset_repr import build_dataset_summary_repr
|
||||
|
||||
assert datasets is not None, "Expected caller to pass in non-None argument"
|
||||
|
||||
formatted = {}
|
||||
for key, dataset in datasets.items():
|
||||
if isinstance(dataset, Dataset):
|
||||
formatted[key] = build_dataset_summary_repr(dataset)
|
||||
else:
|
||||
formatted[key] = dataset
|
||||
|
||||
return "{" + ", ".join(f"'{key}': {formatted[key]}" for key in datasets) + "}"
|
||||
Reference in New Issue
Block a user