chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,16 @@
from .accelerators import AcceleratorSetupCallback
from .backend_setup import BackendSetupCallback
from .datasets import DatasetsCallback
from .state_manager import StateManagerCallback
from .working_dir_setup import WorkingDirectorySetupCallback
__all__ = [
"AcceleratorSetupCallback",
"BackendSetupCallback",
"DatasetsCallback",
"StateManagerCallback",
"WorkingDirectorySetupCallback",
]
# DO NOT ADD ANYTHING AFTER THIS LINE.
@@ -0,0 +1,160 @@
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List
import ray
import ray._private.ray_constants as ray_constants
from ray._private.accelerators.nvidia_gpu import CUDA_VISIBLE_DEVICES_ENV_VAR
from ray._private.ray_constants import env_bool
from ray.train import BackendConfig
from ray.train.constants import ENABLE_SHARE_CUDA_VISIBLE_DEVICES_ENV
from ray.train.v2._internal.execution.callback import WorkerGroupCallback
from ray.train.v2._internal.execution.worker_group import ActorMetadata
from ray.train.v2.api.config import ScalingConfig
if TYPE_CHECKING:
from ray.train.v2._internal.execution.worker_group.worker import Worker
logger = logging.getLogger(__name__)
class AcceleratorSetupCallback(WorkerGroupCallback):
"""Perform accelerator setup for workers.
For example, this callback can be used to share CUDA_VISIBLE_DEVICES
among workers on the same node.
"""
def __init__(self, backend_config: BackendConfig, scaling_config: ScalingConfig):
self._backend = backend_config.backend_cls()
self._scaling_config = scaling_config
def before_init_train_context(
self, workers: List["Worker"]
) -> Dict[str, List[Any]]:
self._maybe_share_cuda_visible_devices(workers)
# TODO: Add support for sharing other accelerator resources.
return {}
def _maybe_share_cuda_visible_devices(self, workers: List["Worker"]):
"""Set CUDA visible devices environment variables on workers."""
share_cuda_visible_devices_enabled = env_bool(
ENABLE_SHARE_CUDA_VISIBLE_DEVICES_ENV,
self._backend.share_cuda_visible_devices,
)
if (
self._scaling_config._resources_per_worker_not_none.get("GPU", 0) > 0
and share_cuda_visible_devices_enabled
):
_share_cuda_visible_devices(workers)
def _share_cuda_visible_devices(workers: List["Worker"]):
"""Sets CUDA_VISIBLE_DEVICES on all workers.
For each worker, CUDA_VISIBLE_DEVICES will be set to the GPU IDs
visible to all workers on that worker's node.
This allows GPU workers on the same node to communicate with one
another.
Example:
Setup:
- Node1:
- Worker1: {0, 1}
- Worker2: {2, 3}
- Node2:
- Worker3: {0, 1}
CUDA_VISIBLE_DEVICES:
- Worker1: "0,1,2,3"
- Worker2: "0,1,2,3"
- Worker3: "0,1"
Args:
workers: List of worker objects.
"""
_share_accelerator_ids(workers, ray_constants.GPU, CUDA_VISIBLE_DEVICES_ENV_VAR)
def _share_accelerator_ids(
workers: List["Worker"], accelerator_name: str, env_var: str
):
"""Sets the given env_var on all workers.
For each worker, the cores/devices are visible to all the
workers on that worker's node. This allows workers on the
same node to communicate with one another.
Example:
Setup:
- Node1:
- Worker1: {0, 1}
- Worker2: {2, 3}
- Node2:
- Worker3: {0, 1}
NEURON_RT_VISIBLE_CORES/TPU_VISIBLE_CHIPS/...:
- Worker1: "0,1,2,3"
- Worker2: "0,1,2,3"
- Worker3: "0,1"
Args:
workers: List of worker objects.
accelerator_name: The name of the accelerator.
env_var: The name of the environment variable to set.
"""
worker_metadatas = [worker.metadata for worker in workers]
visible_accelerator_ids_per_worker = _get_visible_accelerator_ids_per_worker(
worker_metadatas=worker_metadatas, accelerator_name=accelerator_name
)
def set_accelerator_ids(accelerator_ids):
os.environ[env_var] = accelerator_ids
futures = []
for rank, visible_accelerator_ids in enumerate(visible_accelerator_ids_per_worker):
futures.append(
workers[rank].execute_async(
set_accelerator_ids, accelerator_ids=visible_accelerator_ids
)
)
ray.get(futures)
def _get_visible_accelerator_ids_per_worker(
worker_metadatas: List[ActorMetadata], accelerator_name: str
) -> List[str]:
"""Returns a list of comma-separated accelerator IDs visible to each worker.
All workers on a node should have the same set of visible accelerators,
which is the union of accelerator ids of the workers.
Args:
worker_metadatas: The actor metadata for each worker.
accelerator_name: The name of the accelerator resource to inspect.
Returns:
A list of comma-separated accelerator ID strings. This list is the
same length as the number of workers.
"""
for metadata in worker_metadatas:
if accelerator_name not in metadata.accelerator_ids:
raise ValueError(
f"Accelerator '{accelerator_name}' is not available on all workers. "
f"Got these available accelerators instead: {metadata.accelerator_ids}"
)
node_id_to_accelerator_ids = defaultdict(set)
for metadata in worker_metadatas:
node_id_to_accelerator_ids[metadata.node_id].update(
metadata.accelerator_ids[accelerator_name]
)
visible_accelerator_ids_per_worker = []
for worker_id in range(len(worker_metadatas)):
node_id = worker_metadatas[worker_id].node_id
accelerator_ids = sorted(node_id_to_accelerator_ids[node_id])
all_resource_ids = ",".join([str(id) for id in accelerator_ids])
visible_accelerator_ids_per_worker.append(all_resource_ids)
return visible_accelerator_ids_per_worker
@@ -0,0 +1,33 @@
import logging
from ray.exceptions import RayActorError, RayTaskError
from ray.train.backend import BackendConfig
from ray.train.v2._internal.execution.callback import (
ReplicaGroupCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.worker_group import (
ExecutionGroup,
)
logger = logging.getLogger(__name__)
class BackendSetupCallback(ReplicaGroupCallback, WorkerGroupCallback):
def __init__(self, backend_config: BackendConfig):
self._backend_config = backend_config
self._backend = backend_config.backend_cls()
def after_execution_group_start(self, execution_group: ExecutionGroup):
self._backend.on_start(execution_group, self._backend_config)
self._backend.on_training_start(execution_group, self._backend_config)
def before_execution_group_shutdown(self, execution_group: ExecutionGroup):
try:
self._backend.on_shutdown(execution_group, self._backend_config)
except (RayActorError, RayTaskError):
logger.warning(
"Graceful shutdown of backend failed. This is "
"expected if one of the workers has crashed.",
exc_info=True,
)
@@ -0,0 +1,175 @@
import copy
import logging
from typing import TYPE_CHECKING, Dict, List, Optional
import ray
import ray.train
from ray.train.v2._internal.data_integration.interfaces import (
DatasetShardMetadata,
DatasetShardProvider,
GenDataset,
)
from ray.train.v2._internal.execution.callback import WorkerGroupCallback
from ray.train.v2._internal.execution.context import TrainRunContext
from ray.train.v2._internal.execution.worker_group.worker_group import (
Worker,
WorkerGroup,
WorkerGroupContext,
)
from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy
if TYPE_CHECKING:
from ray.data import DataIterator, Dataset, NodeIdStr
from ray.data.context import DataContext
logger = logging.getLogger(__name__)
class RayDatasetShardProvider:
def __init__(
self,
datasets: Dict[str, GenDataset],
data_config: ray.train.DataConfig,
data_context: "DataContext",
world_size: int,
worker_node_ids: List["NodeIdStr"],
):
from ray.train.v2._internal.data_integration.dataset_manager import (
DatasetManager,
)
self._dataset_names = set(datasets)
self._dataset_manager = (
ray.remote(DatasetManager)
.options(
num_cpus=0,
scheduling_strategy=NodeAffinitySchedulingStrategy(
ray.get_runtime_context().get_node_id(), soft=False
),
)
.remote(
datasets=datasets,
data_config=data_config,
data_context=data_context,
world_size=world_size,
worker_node_ids=worker_node_ids,
)
)
self._cached_dataset_shards: Dict[str, "DataIterator"] = {}
def get_dataset_shard(self, dataset_info: DatasetShardMetadata) -> "DataIterator":
dataset_name = dataset_info.dataset_name
if dataset_name not in self._dataset_names:
raise KeyError(
f"Dataset shard for '{dataset_name}' not found. "
"Please ensure that the dataset is passed through the Trainer `datasets` "
"argument."
)
if dataset_name not in self._cached_dataset_shards:
self._cached_dataset_shards[dataset_name] = ray.get(
self._dataset_manager.get_dataset_shard.remote(dataset_info)
)
return self._cached_dataset_shards[dataset_name]
def shutdown_data_executors(self) -> None:
"""
Attempts to eagerly shutdown the data executors for datasets, freeing resources allocated to data execution.
"""
try:
self._dataset_manager.shutdown_data_executors.remote()
except Exception:
logger.debug("Failed to invoke remote cleanup of Dataset Manager.")
class DatasetsCallback(WorkerGroupCallback):
"""A callback for managing Ray Datasets for the worker group."""
def __init__(
self,
train_run_context: TrainRunContext,
datasets: Dict[str, "Dataset"],
):
self._datasets = datasets
self._data_config = copy.deepcopy(train_run_context.dataset_config)
self._scaling_config = train_run_context.scaling_config
self._dataset_shard_provider: Optional[RayDatasetShardProvider] = None
# Capture the current DataContext to propagate it to
# the Train workers later.
# The propagation works in the following way:
# 1. This callback is created when user create the Trainer.
# 2. Then this callback will be passed to the Controller actor.
# 3. Lastly, when the worker group is initialized, the Controller
# will call the `after_worker_group_start` callback to propagate
# the DataContext to Train workers.
from ray.data.context import DataContext
self._data_context = copy.deepcopy(DataContext.get_current())
def get_train_total_resources(
self, scaling_config: ray.train.ScalingConfig
) -> Dict[str, float]:
"""Return the resources reserved for training, so that Data can exclude
these resources logically from its available pool."""
if scaling_config.elasticity_enabled:
# If Train is running with a variable number of workers,
# we can't provide a fixed number of resources to exclude.
# Instead, Train and Data should coordinate via the autoscaling
# coordinator to allocate resources dynamically.
return {}
return scaling_config.total_resources
# --------------------------
# WorkerGroupCallback
# --------------------------
def before_init_train_context(
self, workers: List[Worker]
) -> Dict[str, List[DatasetShardProvider]]:
world_size = len(workers)
worker_node_ids = [worker.metadata.node_id for worker in workers]
datasets = {k: v() if callable(v) else v for k, v in self._datasets.items()}
# TODO: Move this to the constructor.
# Notify the DataConfig about the total resources reserved for training.
total_train_resources = self.get_train_total_resources(self._scaling_config)
self._data_config.set_train_total_resources(
total_train_resources.get("CPU", 0), total_train_resources.get("GPU", 0)
)
self._dataset_shard_provider = RayDatasetShardProvider(
datasets=datasets,
data_config=self._data_config,
data_context=self._data_context,
world_size=world_size,
worker_node_ids=worker_node_ids,
)
return {"dataset_shard_provider": [self._dataset_shard_provider] * world_size}
def after_worker_group_start(self, worker_group: WorkerGroup):
# Propagate DataContext
from ray.data.context import DataContext
def _propagate_data_context(ctx: "DataContext"):
DataContext._set_current(ctx)
worker_group.execute(
_propagate_data_context,
self._data_context,
)
def after_worker_group_shutdown(
self, worker_group_context: WorkerGroupContext
) -> None:
shard_provider = self._dataset_shard_provider
if shard_provider:
shard_provider.shutdown_data_executors()
def after_worker_group_abort(
self, worker_group_context: WorkerGroupContext
) -> None:
shard_provider = self._dataset_shard_provider
if shard_provider:
shard_provider.shutdown_data_executors()
@@ -0,0 +1,39 @@
import importlib
import os
from typing import List
from ray.train.v2._internal.constants import RAY_TRAIN_CALLBACKS_ENV_VAR
from ray.train.v2._internal.execution.callback import RayTrainCallback
def _initialize_env_callbacks() -> List[RayTrainCallback]:
"""Initialize callbacks from environment variable.
Returns:
List of callbacks initialized from environment variable.
"""
callbacks = []
callbacks_str = os.environ.get(RAY_TRAIN_CALLBACKS_ENV_VAR, "")
if not callbacks_str:
return callbacks
for callback_path in callbacks_str.split(","):
callback_path = callback_path.strip()
if not callback_path:
continue
try:
module_path, class_name = callback_path.rsplit(".", 1)
module = importlib.import_module(module_path)
callback_cls = getattr(module, class_name)
if not issubclass(callback_cls, RayTrainCallback):
raise TypeError(
f"Callback class '{callback_path}' must be a subclass of "
f"RayTrainCallback, got {type(callback_cls).__name__}"
)
callback = callback_cls()
callbacks.append(callback)
except (ImportError, AttributeError, ValueError, TypeError) as e:
raise ValueError(f"Failed to import callback from '{callback_path}'") from e
return callbacks
@@ -0,0 +1,124 @@
from contextlib import contextmanager
from typing import Dict, Optional
import ray
from ray.train.v2._internal.execution.callback import (
ControllerCallback,
TrainContextCallback,
WorkerCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.context import TrainRunContext, get_train_context
from ray.train.v2._internal.execution.controller.state import (
TrainControllerState,
TrainControllerStateType,
)
from ray.train.v2._internal.metrics.base import Metric
from ray.train.v2._internal.metrics.controller import ControllerMetrics
from ray.train.v2._internal.metrics.worker import WorkerMetrics
from ray.train.v2._internal.util import time_monotonic
class ControllerMetricsCallback(ControllerCallback, WorkerGroupCallback):
"""Callback that records controller-specific metrics."""
def after_controller_start(self, train_run_context: TrainRunContext):
"""Initialize metrics after controller starts."""
self._run_name = train_run_context.get_run_config().name
self._run_id = train_run_context.run_id
self._metrics: Dict[str, Metric] = ControllerMetrics.get_controller_metrics(
self._run_name, self._run_id
)
# Record initial state
self._metrics[ControllerMetrics.CONTROLLER_STATE].record(
TrainControllerStateType.INITIALIZING
)
async def before_controller_shutdown(self):
"""Shutdown metrics before controller shuts down."""
for metric in self._metrics.values():
metric.reset()
def after_controller_state_update(
self,
previous_state: TrainControllerState,
current_state: TrainControllerState,
):
"""Record state transitions after controller state updates."""
self._metrics[ControllerMetrics.CONTROLLER_STATE].record(
current_state._state_type
)
@contextmanager
def on_worker_group_start(self):
"""Measure time taken to start worker group."""
start_time_s = time_monotonic()
yield
elapsed_time_s = time_monotonic() - start_time_s
self._metrics[ControllerMetrics.WORKER_GROUP_START_TOTAL_TIME_S].record(
elapsed_time_s
)
@contextmanager
def on_worker_group_shutdown(self):
"""Measure time taken to shutdown worker group."""
start_time_s = time_monotonic()
yield
elapsed_time_s = time_monotonic() - start_time_s
self._metrics[ControllerMetrics.WORKER_GROUP_SHUTDOWN_TOTAL_TIME_S].record(
elapsed_time_s
)
class WorkerMetricsCallback(WorkerCallback, TrainContextCallback):
"""Callback that records worker-specific metrics."""
def __init__(self, train_run_context: TrainRunContext):
self._run_name = train_run_context.get_run_config().name
self._run_id = train_run_context.run_id
self._metrics: Optional[Dict[str, Metric]] = None
def after_init_train_context(self):
"""Initialize metrics after train context is initialized."""
train_context = get_train_context()
core_context = ray.runtime_context.get_runtime_context()
world_rank = train_context.get_world_rank()
worker_actor_id = core_context.get_actor_id()
self._metrics = WorkerMetrics.get_worker_metrics(
self._run_name, self._run_id, world_rank, worker_actor_id
)
def before_worker_shutdown(self):
"""Shutdown metrics before shutdown."""
if self._metrics:
for metric in self._metrics.values():
metric.reset()
@contextmanager
def on_report(self):
"""
Context manager to measure the time taken to report a checkpoint to the storage.
"""
start_time_s = time_monotonic()
yield
elapsed_time_s = time_monotonic() - start_time_s
self._metrics[WorkerMetrics.REPORT_TOTAL_BLOCKED_TIME_S].record(elapsed_time_s)
@contextmanager
def on_checkpoint_sync(self):
"""Measure time spent in the cross-rank barrier that synchronizes the
checkpoint directory name across all workers."""
start_time_s = time_monotonic()
yield
elapsed_time_s = time_monotonic() - start_time_s
self._metrics[WorkerMetrics.CHECKPOINT_SYNC_TOTAL_TIME_S].record(elapsed_time_s)
@contextmanager
def on_checkpoint_transfer(self):
"""Measure time spent transferring checkpoint files to storage."""
start_time_s = time_monotonic()
yield
elapsed_time_s = time_monotonic() - start_time_s
self._metrics[WorkerMetrics.CHECKPOINT_TRANSFER_TOTAL_TIME_S].record(
elapsed_time_s
)
@@ -0,0 +1,131 @@
import logging
from typing import TYPE_CHECKING, Optional
import ray
from ray.exceptions import RayActorError
from ray.train.v2._internal.constants import GET_ACTOR_TIMEOUT_S
from ray.train.v2._internal.execution.callback import (
ControllerCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.controller.placement_group_cleaner import (
PlacementGroupCleaner,
)
if TYPE_CHECKING:
from ray.train.v2._internal.execution.context import TrainRunContext
from ray.train.v2._internal.execution.worker_group import WorkerGroup
logger = logging.getLogger(__name__)
class PlacementGroupCleanerCallback(ControllerCallback, WorkerGroupCallback):
"""Callback that manages a PlacementGroupCleaner for the training controller.
This callback ensures that placement groups are cleaned up even if the controller
dies ungracefully.
"""
def __init__(
self,
check_interval_s: float = 1.0,
get_actor_timeout_s: float = GET_ACTOR_TIMEOUT_S,
stop_timeout: Optional[float] = None,
):
"""Initialize the callback.
Args:
check_interval_s: How often (in seconds) the cleaner should check
if the controller is still alive.
get_actor_timeout_s: How long to wait when calling the get actor state api.
stop_timeout: How long to wait for the cleaner to stop.
"""
self._check_interval_s = check_interval_s
self._get_actor_timeout_s = get_actor_timeout_s
self._stop_timeout = stop_timeout
if self._stop_timeout is None:
self._stop_timeout = max(
2.0, self._check_interval_s * 2 + self._get_actor_timeout_s
)
self._cleaner: Optional[PlacementGroupCleaner] = None
self._controller_actor_id: Optional[str] = None
def after_controller_start(self, train_run_context: "TrainRunContext"):
"""Launch the detached PlacementGroupCleaner actor and start monitoring."""
core_context = ray.runtime_context.get_runtime_context()
self._controller_actor_id = core_context.get_actor_id()
try:
# Launch the cleaner as a detached actor so it survives controller death
cleaner_actor_cls = ray.remote(num_cpus=0)(PlacementGroupCleaner)
self._cleaner = cleaner_actor_cls.options(
lifetime="detached",
get_if_exists=False,
).remote(
controller_actor_id=self._controller_actor_id,
check_interval_s=self._check_interval_s,
get_actor_timeout_s=self._get_actor_timeout_s,
stop_timeout=self._stop_timeout,
)
logger.debug(
f"PlacementGroupCleaner launched for run_id={train_run_context.run_id}"
)
except Exception as e:
logger.warning(
f"Failed to launch PlacementGroupCleaner: {e}. "
"Placement groups may not be cleaned up if controller exits ungracefully."
)
self._cleaner = None
return
self._cleaner.start_monitoring.remote()
def after_worker_group_start(self, worker_group: "WorkerGroup"):
"""Register the worker group's placement group with the cleaner.
This is called after a worker group is successfully started.
"""
if not self._cleaner or not self._controller_actor_id:
logger.warning(
"PlacementGroupCleaner not available. "
"Placement groups may not be cleaned up if controller exits ungracefully."
)
return
worker_group_state = worker_group.get_worker_group_state()
placement_group = worker_group_state.placement_group_handle.placement_group
try:
ray.get(self._cleaner.register_placement_group.remote(placement_group))
except Exception as e:
logger.warning(
f"Failed to register placement group with cleaner: {e}. "
"Placement group may not be cleaned up if controller dies ungracefully."
)
return
logger.debug(
f"Registered placement group {placement_group.id} with PlacementGroupCleaner."
)
async def before_controller_shutdown(self):
self._stop_cleaner()
def before_controller_abort(self):
self._stop_cleaner()
def _stop_cleaner(self):
if not self._cleaner:
return
try:
# Stop the cleaner gracefully (it won't clean up the PG)
ray.get(self._cleaner.stop.remote(), timeout=self._stop_timeout)
except RayActorError:
logger.debug(
"PlacementGroupCleaner exited before stop completed; ignoring."
)
except Exception:
logger.exception("Failed to stop PlacementGroupCleaner gracefully.")
finally:
self._cleaner = None
@@ -0,0 +1,93 @@
import logging
import os
from typing import TYPE_CHECKING, Dict, List, Optional
import ray
from ray.actor import ActorHandle
from ray.train.v2._internal.constants import (
DEFAULT_PREEMPTION_POLL_INTERVAL_S,
PREEMPTION_POLL_INTERVAL_S_ENV_VAR,
)
from ray.train.v2._internal.execution.callback import WorkerGroupCallback
from ray.train.v2._internal.execution.preemption import PreemptionWatcher
if TYPE_CHECKING:
from ray.train.v2._internal.execution.worker_group import (
WorkerGroup,
WorkerGroupContext,
)
logger = logging.getLogger(__name__)
class PreemptionCallback(WorkerGroupCallback):
"""Manages a :class:`PreemptionWatcher` across worker-group lifecycles.
Spawns a fresh watcher in :meth:`after_worker_group_start` and stops it on
every teardown path (shutdown and abort). Each worker group gets its own
watcher and failure-domain map, so elastic resizes and restarts never
leak stale state.
"""
def __init__(self) -> None:
self._poll_interval_s: float = float(
os.getenv(
PREEMPTION_POLL_INTERVAL_S_ENV_VAR,
str(DEFAULT_PREEMPTION_POLL_INTERVAL_S),
)
)
self._watcher: Optional[ActorHandle] = None
def after_worker_group_start(self, worker_group: "WorkerGroup") -> None:
# Tear down any watcher from a previous worker group first. Worker-group
# startup can fail after this hook without running the shutdown hook, so
# this also prevents leaking an orphaned watcher across a reschedule.
self._stop_watcher()
# These handles are captured once per worker-group start. With the
# standard backend, any worker replacement goes through a full worker
# group restart (this hook runs again with fresh handles), so they
# never go stale.
# TODO(lehui): refresh worker handles on in-place replica replacement
# when adding preemption support for replica groups (TorchFT).
node_to_ranks: Dict[str, List[int]] = {}
worker_actors_by_rank: Dict[int, ActorHandle] = {}
for w in worker_group.get_workers():
rank = w.distributed_context.world_rank
node_to_ranks.setdefault(w.metadata.node_id, []).append(rank)
worker_actors_by_rank[rank] = w.actor
watcher_cls = ray.remote(num_cpus=0, max_restarts=-1)(PreemptionWatcher)
self._watcher = watcher_cls.remote(
node_to_ranks=node_to_ranks,
poll_interval_s=self._poll_interval_s,
worker_actors_by_rank=worker_actors_by_rank,
)
logger.debug(
"PreemptionCallback: started watcher for %d node(s).",
len(node_to_ranks),
)
def before_worker_group_shutdown(self, worker_group: "WorkerGroup") -> None:
self._stop_watcher()
def after_worker_group_abort(
self, worker_group_context: "WorkerGroupContext"
) -> None:
# abort() doesn't run the shutdown hook, so tear the watcher down here
# too — otherwise it keeps polling GCS until the cluster reaps it.
self._stop_watcher()
def _stop_watcher(self) -> None:
if self._watcher is None:
return
watcher = self._watcher
self._watcher = None
# Force-kill (non-blocking) rather than a synchronous graceful stop, so
# we never block the controller's event loop. The watcher's daemon poll
# thread dies with the actor process and holds no external resources.
try:
ray.kill(watcher)
except Exception:
logger.warning("Failed to kill PreemptionWatcher actor.", exc_info=True)
@@ -0,0 +1,221 @@
import importlib
import logging
from typing import TYPE_CHECKING, Dict, Optional
if TYPE_CHECKING:
from ray.data import Dataset
import ray
from ray.train.v2._internal.execution.callback import (
ControllerCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.context import TrainRunContext
from ray.train.v2._internal.execution.controller.state import (
AbortedState,
ErroredState,
FinishedState,
ReschedulingState,
ResizingState,
RestartingState,
RunningState,
SchedulingState,
ShuttingDownState,
TrainControllerState,
)
from ray.train.v2._internal.execution.scaling_policy.scaling_policy import (
ResizeDecision,
)
from ray.train.v2._internal.execution.worker_group import (
WorkerGroup,
WorkerGroupContext,
WorkerGroupState,
)
from ray.train.v2._internal.execution.worker_group.poll import WorkerGroupPollStatus
from ray.train.v2._internal.logging.logging import (
get_train_application_controller_log_path,
)
from ray.train.v2._internal.state.state_manager import TrainStateManager
from ray.train.v2._internal.util import TrainingFramework
logger = logging.getLogger(__name__)
def _get_framework_version(framework: Optional[TrainingFramework]):
versions = {}
try:
import ray
versions["ray"] = ray.__version__
except ImportError:
logger.warning("Failed to collect ray version on worker.")
if framework is None:
return versions
for module_name in framework.module_names():
try:
module = importlib.import_module(module_name)
versions[module_name] = module.__version__
except ModuleNotFoundError:
# Module is not installed, skip without recording a version.
continue
except Exception:
logger.warning(f"Failed to collect {module_name} version on worker.")
continue
return versions
class StateManagerCallback(ControllerCallback, WorkerGroupCallback):
def __init__(self, datasets: Dict[str, "Dataset"]):
self._datasets = datasets
def after_controller_start(self, train_run_context: TrainRunContext):
self._state_manager = TrainStateManager()
self._run_name = train_run_context.get_run_config().name
self._run_id = train_run_context.run_id
# TODO: Should this be generated by the caller?
# NOTE: These must be called on the Controller.
# The Callback is first initialized on the Driver.
core_context = ray.runtime_context.get_runtime_context()
self._job_id = core_context.get_job_id()
self._controller_actor_id = core_context.get_actor_id()
controller_log_file_path = get_train_application_controller_log_path()
self._state_manager.create_train_run(
id=self._run_id,
name=self._run_name,
job_id=self._job_id,
controller_actor_id=self._controller_actor_id,
controller_log_file_path=controller_log_file_path,
run_config=train_run_context.run_config,
train_loop_config=train_run_context.train_loop_config,
scaling_config=train_run_context.scaling_config,
backend_config=train_run_context.backend_config,
datasets=self._datasets,
dataset_config=train_run_context.dataset_config,
)
def after_controller_state_update(
self,
previous_state: TrainControllerState,
current_state: TrainControllerState,
):
if previous_state._state_type == current_state._state_type:
return
logger.info(
f"[State Transition] {previous_state._state_type.state_name} -> "
f"{current_state._state_type.state_name}."
)
if isinstance(current_state, SchedulingState):
# TODO: This should probably always be ResizeDecision.
if isinstance(current_state.scaling_decision, ResizeDecision):
resize_decision = current_state.scaling_decision
else:
resize_decision = None
self._state_manager.update_train_run_scheduling(
run_id=self._run_id,
resize_decision=resize_decision,
)
elif isinstance(current_state, RunningState):
self._state_manager.update_train_run_running(
run_id=self._run_id,
)
elif isinstance(current_state, RestartingState):
self._state_manager.update_train_run_restarting(
run_id=self._run_id,
)
elif isinstance(current_state, ResizingState):
self._state_manager.update_train_run_resizing(
run_id=self._run_id,
)
elif isinstance(current_state, ErroredState):
self._state_manager.update_train_run_errored(
run_id=self._run_id,
status_detail=str(current_state.training_failed_error),
)
elif isinstance(current_state, FinishedState):
self._state_manager.update_train_run_finished(
run_id=self._run_id,
)
elif isinstance(current_state, AbortedState):
self._state_manager.update_train_run_aborted(
run_id=self._run_id,
)
elif isinstance(current_state, ReschedulingState):
# substate of SchedulingState
pass
elif isinstance(current_state, ShuttingDownState):
# substate of RunningState
pass
def before_worker_group_start(self, worker_group_context: WorkerGroupContext):
self._state_manager.create_train_run_attempt(
run_id=self._run_id,
attempt_id=worker_group_context.run_attempt_id,
num_workers=worker_group_context.num_workers,
resources_per_worker=worker_group_context.resources_per_worker,
)
def after_worker_group_start(self, worker_group: WorkerGroup):
worker_group_context: WorkerGroupContext = (
worker_group.get_worker_group_context()
)
worker_group_state: WorkerGroupState = worker_group.get_worker_group_state()
self._state_manager.update_train_run_attempt_running(
run_id=self._run_id,
attempt_id=worker_group_context.run_attempt_id,
workers=worker_group_state.workers,
)
# Update train run framework version
framework = self._state_manager.get_train_run_framework(self._run_id)
framework_versions = worker_group.execute_single(
0, _get_framework_version, framework
)
self._state_manager.update_train_run_framework_versions(
run_id=self._run_id,
framework_versions=framework_versions,
)
def before_worker_group_shutdown(self, worker_group: WorkerGroup):
worker_group_context: WorkerGroupContext = (
worker_group.get_worker_group_context()
)
# TODO: Consider passing error reason directly to the callback.
# Something along the lines of:
# WorkerGroup.shutdown(reason)
# -> WorkerGroupCallback.before_worker_group_shutdown(reason)
worker_group_poll_status: Optional[
WorkerGroupPollStatus
] = worker_group.get_latest_poll_status()
if worker_group_poll_status and worker_group_poll_status.errors:
self._state_manager.update_train_run_attempt_errored(
run_id=self._run_id,
attempt_id=worker_group_context.run_attempt_id,
status_detail=worker_group_poll_status.get_error_string(),
)
else:
self._state_manager.update_train_run_attempt_finished(
run_id=self._run_id,
attempt_id=worker_group_context.run_attempt_id,
)
def before_worker_group_abort(self, worker_group_context: WorkerGroupContext):
self._state_manager.update_train_run_attempt_aborted(
self._run_id,
worker_group_context.run_attempt_id,
)
@@ -0,0 +1,54 @@
from typing import Any, Dict, List
from ray.train.v2._internal.execution.callback import (
ReportCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.context import TrainRunContext
from ray.train.v2._internal.execution.training_report import _TrainingReport
from ray.train.v2._internal.execution.worker_group import WorkerGroupPollStatus
from ray.train.v2.api.callback import UserCallback
class UserCallbackHandler(WorkerGroupCallback, ReportCallback):
"""Responsible for calling methods of subscribers implementing
the `UserCallback` interface.
"""
def __init__(
self, user_callbacks: List[UserCallback], train_run_context: TrainRunContext
):
self._user_callbacks = user_callbacks
self._train_run_context = train_run_context
# --------------------------
# ReportCallback
# --------------------------
def after_report(
self,
training_report: _TrainingReport,
metrics: List[Dict[str, Any]],
):
for user_callback in self._user_callbacks:
user_callback.after_report(
run_context=self._train_run_context,
metrics=metrics,
checkpoint=training_report.checkpoint,
)
# --------------------------
# WorkerGroupCallback
# --------------------------
def after_worker_group_poll_status(
self, worker_group_status: WorkerGroupPollStatus
):
if not worker_group_status.errors:
return
for user_callback in self._user_callbacks:
user_callback.after_exception(
run_context=self._train_run_context,
worker_exceptions=worker_group_status.errors,
)
@@ -0,0 +1,31 @@
import logging
import os
from ray.train.v2._internal.execution.callback import (
ReplicaGroupCallback,
WorkerGroupCallback,
)
from ray.train.v2._internal.execution.context import get_train_context
from ray.train.v2._internal.execution.worker_group import (
ExecutionGroup,
)
logger = logging.getLogger(__name__)
class WorkingDirectorySetupCallback(ReplicaGroupCallback, WorkerGroupCallback):
def after_execution_group_start(self, execution_group: ExecutionGroup):
"""Shared logic for setting up the working directory on an execution group."""
def chdir_to_working_dir() -> None:
"""Create the local working directory for the experiment."""
local_working_directory = (
get_train_context().get_storage().local_working_directory
)
os.makedirs(local_working_directory, exist_ok=True)
logger.debug(
f"Changing the working directory to: {local_working_directory}"
)
os.chdir(local_working_directory)
execution_group.execute(chdir_to_working_dir)