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

528 lines
22 KiB
Python

import logging
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
import pyarrow.fs
from ray.air.config import (
FailureConfig as FailureConfigV1,
ScalingConfig as ScalingConfigV1,
)
from ray.runtime_env import RuntimeEnv
from ray.train.v2._internal.constants import _DEPRECATED
from ray.train.v2._internal.execution.storage import StorageContext
from ray.train.v2._internal.migration_utils import (
FAIL_FAST_DEPRECATION_MESSAGE,
TRAINER_RESOURCES_DEPRECATION_MESSAGE,
)
from ray.train.v2._internal.util import date_str
from ray.util.annotations import PublicAPI
from ray.util.tpu import get_tpu_worker_resources
if TYPE_CHECKING:
from ray.train import UserCallback
logger = logging.getLogger(__name__)
@dataclass
class ScalingConfig(ScalingConfigV1):
"""Configuration for scaling training.
Args:
num_workers: The number of workers (Ray actors) to launch.
Each worker will reserve 1 CPU by default. The number of CPUs
reserved by each worker can be overridden with the
``resources_per_worker`` argument. If the number of workers is 0,
the training function will run in local mode, meaning the training
function runs in the same process. To enable elasticity, provide a
``(min_workers, max_workers)`` tuple of ints.
elastic_resize_monitor_interval_s: While the worker group is healthy,
consider resizing the worker group every
``elastic_resize_monitor_interval_s`` seconds.
use_gpu: If True, training will be done on GPUs (1 per worker).
Defaults to False. The number of GPUs reserved by each
worker can be overridden with the ``resources_per_worker``
argument.
resources_per_worker: If specified, the resources
defined in this Dict is reserved for each worker.
Define the ``"CPU"`` and ``"GPU"`` keys (case-sensitive) to
override the number of CPU or GPUs used by each worker.
Accepts the same resource keys that Ray uses for scheduling tasks
and actors (see :ref:`Resources <core-resources>`):
- ``"CPU"``: number of logical CPUs per worker.
- ``"GPU"``: number of logical GPUs per worker. Prefer setting
``use_gpu=True`` (which reserves 1 GPU per worker) and only
override this key when you need a different per-worker count.
- ``"TPU"``: number of logical TPUs per worker, when ``use_tpu=True``.
- ``"memory"``: heap memory reserved per worker, in bytes
(for example, ``"memory": 1e9`` reserves 1 GB per worker).
- Any :ref:`custom resource <custom-resources>` name configured on
your cluster (for example, ``"special_hardware": 1``).
Keys are case-sensitive: use ``"CPU"``, ``"GPU"``, and ``"TPU"``
(uppercase), and ``"memory"`` (lowercase).
placement_strategy: The placement strategy to use for the
placement group of the Ray actors. See :ref:`Placement Group
Strategies <pgroup-strategy>` for the possible options.
label_selector: A list of label selectors for Ray Train worker placement.
If a single label selector is provided, it will be applied to all Ray Train workers.
If a list is provided, it must be the same length as the max number of Ray Train workers.
accelerator_type: [Experimental] If specified, Ray Train will launch the
training coordinator and workers on the nodes with the specified type
of accelerators.
See :ref:`the available accelerator types <accelerator_types>`.
Ensure that your cluster has instances with the specified accelerator type
or is able to autoscale to fulfill the request. This field is required
when `use_tpu` is True and `num_workers` is greater than 1.
use_tpu: [Experimental] If True, training will be done on TPUs (1 TPU VM
per worker). Defaults to False. The number of TPUs reserved by each
worker can be overridden with the ``resources_per_worker``
argument. This arg enables SPMD execution of the training workload.
topology: [Experimental] If specified, Ray Train will launch the training
coordinator and workers on nodes with the specified topology. Topology is
auto-detected for TPUs and added as Ray node labels. This arg enables
SPMD execution of the training workload. This field is required
when `use_tpu` is True and `num_workers` is greater than 1.
"""
num_workers: Union[int, Tuple[int, int]] = 1
trainer_resources: Optional[dict] = None
label_selector: Optional[Union[Dict[str, str], List[Dict[str, str]]]] = None
# Accelerator specific fields.
use_tpu: Union[bool] = False
topology: Optional[str] = None
# Elasticity specific fields.
elastic_resize_monitor_interval_s: float = 60.0
def __post_init__(self):
if self.trainer_resources is not None:
raise DeprecationWarning(TRAINER_RESOURCES_DEPRECATION_MESSAGE)
is_fixed = isinstance(self.num_workers, int)
is_elastic = (
isinstance(self.num_workers, tuple)
and len(self.num_workers) == 2
and all(isinstance(x, int) for x in self.num_workers)
)
if not (is_fixed or is_elastic):
raise ValueError(
"ScalingConfig(num_workers) must be an int or a tuple of two ints."
)
if self.elastic_resize_monitor_interval_s < 0:
raise ValueError(
"ScalingConfig(elastic_resize_monitor_interval_s) must be non-negative."
)
if self.min_workers < 0:
raise ValueError(
f"Invalid ScalingConfig(num_workers={self.num_workers}): "
"Number of workers cannot be negative."
)
if self.min_workers > self.max_workers:
raise ValueError(
f"Invalid ScalingConfig(num_workers={self.num_workers}): "
f"min_workers={self.min_workers} must be <= max_workers={self.max_workers}."
)
self._validate_tpu_config()
if (
isinstance(self.label_selector, list)
and len(self.label_selector) != self.max_workers
):
raise ValueError(
"If `label_selector` is a list, it must be the same length as "
"`max_workers` (or `num_workers` when fixed)."
)
if self.num_workers == 0:
logger.info(
"Running in local mode. The training function will run in the same process. "
"If you are using it and running into issues please file a report at "
"https://github.com/ray-project/ray/issues."
)
super().__post_init__()
@property
def elasticity_enabled(self) -> bool:
return isinstance(self.num_workers, tuple)
@property
def min_workers(self) -> int:
return (
self.num_workers
if isinstance(self.num_workers, int)
else self.num_workers[0]
)
@property
def max_workers(self) -> int:
return (
self.num_workers
if isinstance(self.num_workers, int)
else self.num_workers[1]
)
def _label_selector_per_worker(
self, num_workers: int
) -> Optional[List[Dict[str, str]]]:
"""Normalize ``label_selector`` into a per-worker list of length ``num_workers``.
- ``None`` -> ``None`` (no constraint; downstream consumers — the
placement-group path and the autoscaling coordinator — both
accept ``None`` and treat it as "no label requirement").
- ``Dict`` -> the same dict replicated for each worker
- ``List`` -> the first ``num_workers`` entries (validated to be
``max_workers`` long in ``__post_init__``)
"""
if isinstance(self.label_selector, list):
return [s.copy() for s in self.label_selector[:num_workers]]
if isinstance(self.label_selector, dict):
return [self.label_selector.copy() for _ in range(num_workers)]
return None
@property
def total_resources(self):
"""Map of total resources required for training.
For elastic configs, this returns an upper bound based on max_workers.
"""
total_resource_map = dict(self._trainer_resources_not_none)
for k, value in self._resources_per_worker_not_none.items():
total_resource_map[k] = total_resource_map.get(k, 0.0) + (
value * self.max_workers
)
return total_resource_map
def _validate_tpu_config(self):
"""Validates configuration specifically for TPU usage."""
max_workers = self.max_workers
if self.use_gpu and self.use_tpu:
raise ValueError("Cannot specify both `use_gpu=True` and `use_tpu=True`.")
if not self.use_tpu:
if self.num_tpus_per_worker > 0:
raise ValueError(
"`use_tpu` is False but `TPU` was found in "
"`resources_per_worker`. Either set `use_tpu` to True or "
"remove `TPU` from `resources_per_worker."
)
# If not using TPU, we are done validating TPU-specific logic.
return
if self.num_tpus_per_worker == 0:
raise ValueError(
"`use_tpu` is True but `TPU` is set to 0 in "
"`resources_per_worker`. Either set `use_tpu` to False or "
"request a positive number of `TPU` in "
"`resources_per_worker."
)
if max_workers > 1:
if not self.topology:
raise ValueError(
"`topology` must be specified in ScalingConfig when `use_tpu=True` "
" and `num_workers` > 1."
)
if not self.accelerator_type:
raise ValueError(
"`accelerator_type` must be specified in ScalingConfig when "
"`use_tpu=True` and `num_workers` > 1."
)
if self.label_selector:
raise ValueError(
"Cannot set `label_selector` when `use_tpu=True` because "
"Ray Train automatically reserves a TPU slice with a predefined label."
)
# Validate TPU resources when both topology and accelerator type are specified.
if self.topology and self.accelerator_type:
try:
workers_per_slice, tpu_resources = get_tpu_worker_resources(
topology=self.topology,
accelerator_type=self.accelerator_type,
resources_per_unit=self.resources_per_worker,
num_slices=1,
)
except Exception as e:
raise ValueError(
f"Could not parse TPU topology details for "
f"type={self.accelerator_type}, "
f"topology={self.topology}. Error: {e}"
)
if workers_per_slice > 0 and max_workers % workers_per_slice != 0:
raise ValueError(
f"The configured `num_workers` ({self.num_workers}) must be a "
f"multiple of {workers_per_slice} for the specified topology ({self.topology}). "
"TPU workloads typically require symmetric resource distribution "
"across all slices to function correctly."
)
if workers_per_slice > 0 and self.min_workers % workers_per_slice != 0:
raise ValueError(
f"The configured `min_workers` ({self.min_workers}) must be a "
f"multiple of {workers_per_slice} for the specified topology ({self.topology}). "
"TPU workloads typically require symmetric resource distribution "
"across all slices to function correctly."
)
if self.resources_per_worker is None:
self.resources_per_worker = tpu_resources
@property
def _resources_per_worker_not_none(self):
if self.resources_per_worker is None:
if self.use_tpu:
return {"TPU": 1}
return super()._resources_per_worker_not_none
@property
def _trainer_resources_not_none(self):
return {}
@property
def num_tpus_per_worker(self):
"""The number of TPUs to set per worker."""
return self._resources_per_worker_not_none.get("TPU", 0)
@dataclass
@PublicAPI(stability="stable")
class CheckpointConfig:
"""Configuration for checkpointing.
Default behavior is to persist all checkpoints reported with
:meth:`ray.train.report` to disk. If ``num_to_keep`` is set,
the default retention policy is to keep the most recent checkpoints.
Args:
num_to_keep: The maximum number of checkpoints to keep.
If you report more checkpoints than this, the oldest
(or lowest-scoring, if ``checkpoint_score_attribute`` is set)
checkpoint will be deleted.
If this is ``None`` then all checkpoints will be kept. Must be >= 1.
checkpoint_score_attribute: The attribute that will be used to
score checkpoints to determine which checkpoints should be kept.
This attribute must be a key from the metrics dictionary
attached to the checkpoint. This attribute must have a numerical value.
checkpoint_score_order: Either "max" or "min".
If "max"/"min", then checkpoints with highest/lowest values of
the ``checkpoint_score_attribute`` will be kept. Defaults to "max".
checkpoint_frequency: [Deprecated]
checkpoint_at_end: [Deprecated]
"""
num_to_keep: Optional[int] = None
checkpoint_score_attribute: Optional[str] = None
checkpoint_score_order: Literal["max", "min"] = "max"
checkpoint_frequency: Union[Optional[int], Literal[_DEPRECATED]] = _DEPRECATED
checkpoint_at_end: Union[Optional[bool], Literal[_DEPRECATED]] = _DEPRECATED
def __post_init__(self):
if self.checkpoint_frequency != _DEPRECATED:
raise DeprecationWarning(
"`checkpoint_frequency` is deprecated since it does not "
"apply to user-defined training functions. "
"Please remove this argument from your CheckpointConfig."
)
if self.checkpoint_at_end != _DEPRECATED:
raise DeprecationWarning(
"`checkpoint_at_end` is deprecated since it does not "
"apply to user-defined training functions. "
"Please remove this argument from your CheckpointConfig."
)
if self.num_to_keep is not None and self.num_to_keep <= 0:
raise ValueError(
f"Received invalid num_to_keep: {self.num_to_keep}. "
"Must be None or an integer >= 1."
)
if self.checkpoint_score_order not in ("max", "min"):
raise ValueError(
f"Received invalid checkpoint_score_order: {self.checkpoint_score_order}. "
"Must be 'max' or 'min'."
)
@dataclass
class FailureConfig(FailureConfigV1):
"""Configuration related to failure handling of each training run.
Args:
max_failures: Tries to recover a run from training worker errors at least this many times.
Will recover from the latest checkpoint if present.
Setting to -1 will lead to infinite recovery retries.
Setting to 0 will disable retries. Defaults to 0.
controller_failure_limit: [DeveloperAPI] The maximum number of controller failures to tolerate.
Setting to -1 will lead to infinite controller retries.
Setting to 0 will disable controller retries. Defaults to -1.
"""
fail_fast: Union[bool, str] = _DEPRECATED
controller_failure_limit: int = -1
def __post_init__(self):
if self.fail_fast != _DEPRECATED:
raise DeprecationWarning(FAIL_FAST_DEPRECATION_MESSAGE)
@PublicAPI(stability="alpha")
@dataclass
class LoggingConfig:
"""Configuration for Ray Train's logging behavior.
Args:
log_level: The log level for Ray Train's internal ``ray.train`` logs
on console output and application-level log files. Accepts standard
Python logging level names. Defaults to ``"INFO"``.
System-level log files always capture all levels (DEBUG and above),
and the ``ray`` logger (set by ``ray.init()``) and root logger
are unaffected.
"""
log_level: str = "INFO"
def __post_init__(self):
valid_levels = set(logging._nameToLevel)
if (
not isinstance(self.log_level, str)
or self.log_level.upper() not in valid_levels
):
raise ValueError(
f"Invalid log_level: {self.log_level!r}. "
f"Must be one of: {', '.join(repr(x) for x in sorted(valid_levels))}."
)
self.log_level = self.log_level.upper()
@dataclass
@PublicAPI(stability="stable")
class RunConfig:
"""Runtime configuration for training runs.
Args:
name: Name of the trial or experiment. If not provided, will be deduced
from the Trainable.
storage_path: Path where all results and checkpoints are persisted.
Can be a local directory or a destination on cloud storage.
For multi-node training/tuning runs, this must be set to a
shared storage location (e.g., S3, NFS).
This defaults to the local ``~/ray_results`` directory.
storage_filesystem: A custom filesystem to use for storage.
If this is provided, `storage_path` should be a path with its
prefix stripped (e.g., `s3://bucket/path` -> `bucket/path`).
failure_config: Failure mode configuration.
checkpoint_config: Checkpointing configuration.
callbacks: [DeveloperAPI] A list of callbacks that the Ray Train controller
will invoke during training.
worker_runtime_env: [DeveloperAPI] Runtime environment configuration
for all Ray Train worker actors.
logging_config: Configuration for Ray Train's logging behavior.
See :class:`LoggingConfig` for details.
"""
name: Optional[str] = None
storage_path: Optional[str] = None
storage_filesystem: Optional[pyarrow.fs.FileSystem] = None
failure_config: Optional[FailureConfig] = None
checkpoint_config: Optional[CheckpointConfig] = None
callbacks: Optional[List["UserCallback"]] = None
worker_runtime_env: Optional[Union[dict, RuntimeEnv]] = None
logging_config: Optional[LoggingConfig] = None
sync_config: str = _DEPRECATED
verbose: str = _DEPRECATED
stop: str = _DEPRECATED
progress_reporter: str = _DEPRECATED
log_to_file: str = _DEPRECATED
def __post_init__(self):
from ray.train.constants import DEFAULT_STORAGE_PATH
if self.storage_path is None:
self.storage_path = DEFAULT_STORAGE_PATH
if not self.failure_config:
self.failure_config = FailureConfig()
if not self.checkpoint_config:
self.checkpoint_config = CheckpointConfig()
if not self.logging_config:
self.logging_config = LoggingConfig()
if isinstance(self.storage_path, Path):
self.storage_path = self.storage_path.as_posix()
run_config_deprecation_message = (
"`RunConfig({})` is deprecated. This configuration was a "
"Ray Tune API that did not support Ray Train usage well, "
"so we are dropping support going forward. "
"If you heavily rely on these configurations, "
"you can run Ray Train as a single Ray Tune trial. "
"See this issue for more context: "
"https://github.com/ray-project/ray/issues/49454"
)
unsupported_params = [
"sync_config",
"verbose",
"stop",
"progress_reporter",
"log_to_file",
]
for param in unsupported_params:
if getattr(self, param) != _DEPRECATED:
raise DeprecationWarning(run_config_deprecation_message.format(param))
if not self.name:
self.name = f"ray_train_run-{date_str()}"
self.callbacks = self.callbacks or []
self.worker_runtime_env = self.worker_runtime_env or {}
from ray.train.v2.api.callback import RayTrainCallback
if not all(isinstance(cb, RayTrainCallback) for cb in self.callbacks):
raise ValueError(
"All callbacks must be instances of `ray.train.UserCallback`. "
"Passing in a Ray Tune callback is no longer supported. "
"See this issue for more context: "
"https://github.com/ray-project/ray/issues/49454"
)
if not isinstance(self.checkpoint_config, CheckpointConfig):
raise ValueError(
f"Invalid `CheckpointConfig` type: {self.checkpoint_config.__class__}. "
"Use `ray.train.CheckpointConfig` instead. "
"See this issue for more context: "
"https://github.com/ray-project/ray/issues/49454"
)
if not isinstance(self.failure_config, FailureConfig):
raise ValueError(
f"Invalid `FailureConfig` type: {self.failure_config.__class__}. "
"Use `ray.train.FailureConfig` instead. "
"See this issue for more context: "
"https://github.com/ray-project/ray/issues/49454"
)
@cached_property
def storage_context(self) -> StorageContext:
return StorageContext(
storage_path=self.storage_path,
experiment_dir_name=self.name,
storage_filesystem=self.storage_filesystem,
)