Files
ray-project--ray/python/ray/train/v2/_internal/state/state_manager.py
T
2026-07-13 13:17:40 +08:00

327 lines
12 KiB
Python

import logging
from collections import defaultdict
from typing import Dict, List, Optional
import ray
from ray.actor import ActorHandle
from ray.train import BackendConfig
from ray.train._internal.data_config import DataConfig
from ray.train.v2._internal.execution.context import DistributedContext
from ray.train.v2._internal.execution.scaling_policy.scaling_policy import (
ResizeDecision,
)
from ray.train.v2._internal.execution.worker_group import ActorMetadata, Worker
from ray.train.v2._internal.state.schema import (
ActorStatus,
BackendConfig as BackendConfigSchema,
CheckpointConfig as CheckpointConfigSchema,
FailureConfig as FailureConfigSchema,
RunAttemptStatus,
RunConfig as RunConfigSchema,
RunSettings,
RunStatus,
ScalingConfig as ScalingConfigSchema,
TrainResources,
TrainRun,
TrainRunAttempt,
TrainWorker,
)
from ray.train.v2._internal.state.state_actor import get_or_create_state_actor
from ray.train.v2._internal.state.util import (
construct_data_config,
current_time_ns,
mark_workers_dead,
update_train_run_aborted,
update_train_run_attempt_aborted,
)
from ray.train.v2._internal.util import TrainingFramework
from ray.train.v2.api.config import RunConfig, ScalingConfig
logger = logging.getLogger(__name__)
class TrainStateManager:
"""Manages the state of a train run and run attempts."""
def __init__(self) -> None:
self._state_actor = get_or_create_state_actor()
# NOTE: All runs and attempts are stored in memory.
# This may be a memory issue for large runs.
self._runs: Dict[str, TrainRun] = {}
# {run_id: {attempt_id: TrainRunAttempt}}
self._run_attempts: Dict[str, Dict[str, TrainRunAttempt]] = defaultdict(dict)
def create_train_run(
self,
id: str,
name: str,
job_id: str,
controller_actor_id: str,
controller_log_file_path: str,
run_config: RunConfig,
train_loop_config: Optional[Dict],
scaling_config: ScalingConfig,
backend_config: BackendConfig,
datasets: Dict[str, ray.data.Dataset],
dataset_config: DataConfig,
) -> None:
run_config_schema = RunConfigSchema(
name=run_config.name,
failure_config=FailureConfigSchema(
max_failures=run_config.failure_config.max_failures,
controller_failure_limit=run_config.failure_config.controller_failure_limit,
),
worker_runtime_env=run_config.worker_runtime_env,
checkpoint_config=CheckpointConfigSchema(
num_to_keep=run_config.checkpoint_config.num_to_keep,
checkpoint_score_attribute=run_config.checkpoint_config.checkpoint_score_attribute,
checkpoint_score_order=run_config.checkpoint_config.checkpoint_score_order,
),
storage_path=run_config.storage_path,
storage_filesystem=run_config.storage_filesystem,
)
scaling_config_schema = ScalingConfigSchema(
num_workers=scaling_config.num_workers,
use_gpu=scaling_config.use_gpu,
resources_per_worker=scaling_config.resources_per_worker,
placement_strategy=scaling_config.placement_strategy,
accelerator_type=scaling_config.accelerator_type,
use_tpu=scaling_config.use_tpu,
topology=scaling_config.topology,
bundle_label_selector=scaling_config.label_selector,
)
backend_config_schema = BackendConfigSchema(
framework=backend_config.framework,
config=backend_config.to_dict(),
)
run_settings = RunSettings(
train_loop_config=train_loop_config,
backend_config=backend_config_schema,
scaling_config=scaling_config_schema,
datasets=list(datasets.keys()),
data_config=construct_data_config(dataset_config),
run_config=run_config_schema,
)
run = TrainRun(
id=id,
name=name,
job_id=job_id,
status=RunStatus.INITIALIZING,
status_detail=None,
controller_actor_id=controller_actor_id,
start_time_ns=current_time_ns(),
end_time_ns=None,
controller_log_file_path=controller_log_file_path,
framework_versions={"ray": ray.__version__},
run_settings=run_settings,
)
self._runs[run.id] = run
# Block so the initial run state isn't lost if the controller exits
# right after. Without this, the .remote() task could still be in the
# caller's outbound queue when the controller dies, leaving the state
# actor with no record of the run.
self._create_or_update_train_run(run, block=True)
def update_train_run_scheduling(
self,
run_id: str,
resize_decision: Optional[ResizeDecision] = None,
) -> None:
if resize_decision is not None:
status_detail = _get_scheduling_status_detail(
resize_decision.num_workers, resize_decision.resources_per_worker
)
else:
status_detail = None
run = self._runs[run_id]
run.status = RunStatus.SCHEDULING
run.status_detail = status_detail
self._create_or_update_train_run(run)
def update_train_run_running(
self,
run_id: str,
) -> None:
run = self._runs[run_id]
run.status = RunStatus.RUNNING
run.status_detail = None
self._create_or_update_train_run(run)
def update_train_run_restarting(
self,
run_id: str,
) -> None:
run = self._runs[run_id]
run.status = RunStatus.RESTARTING
run.status_detail = None
self._create_or_update_train_run(run)
def update_train_run_resizing(
self,
run_id: str,
) -> None:
run = self._runs[run_id]
run.status = RunStatus.RESIZING
run.status_detail = None
self._create_or_update_train_run(run)
def update_train_run_finished(
self,
run_id: str,
):
run = self._runs[run_id]
run.status = RunStatus.FINISHED
run.status_detail = None
run.end_time_ns = current_time_ns()
# Block on terminal status so the final state isn't lost if the controller exits right after.
self._create_or_update_train_run(run, block=True)
def update_train_run_errored(
self,
run_id: str,
status_detail: str,
):
run = self._runs[run_id]
run.status = RunStatus.ERRORED
run.status_detail = status_detail
run.end_time_ns = current_time_ns()
# Block on terminal status so the final state isn't lost if the controller exits right after.
self._create_or_update_train_run(run, block=True)
def update_train_run_aborted(
self,
run_id: str,
):
run = self._runs[run_id]
update_train_run_aborted(run=run, graceful=True)
# Block on terminal status so the final state isn't lost if the controller exits right after.
self._create_or_update_train_run(run, block=True)
def update_train_run_framework_versions(
self, run_id: str, framework_versions: Dict[str, str]
):
run = self._runs[run_id]
run.framework_versions = framework_versions
self._create_or_update_train_run(run)
def create_train_run_attempt(
self,
run_id: str,
attempt_id: str,
num_workers: int,
resources_per_worker: Dict[str, float],
) -> None:
status_detail = _get_scheduling_status_detail(num_workers, resources_per_worker)
resources = [
TrainResources(resources=resources_per_worker) for _ in range(num_workers)
]
run_attempt = TrainRunAttempt(
run_id=run_id,
attempt_id=attempt_id,
start_time_ns=current_time_ns(),
status=RunAttemptStatus.PENDING,
status_detail=status_detail,
resources=resources,
workers=[], # Not started yet.
)
self._run_attempts[run_id][attempt_id] = run_attempt
self._create_or_update_train_run_attempt(run_attempt)
def update_train_run_attempt_running(
self, run_id: str, attempt_id: str, workers: List[Worker]
) -> None:
def _convert_worker(worker: Worker) -> TrainWorker:
actor: ActorHandle = worker.actor
distributed_context: DistributedContext = worker.distributed_context
actor_metadata: ActorMetadata = worker.metadata
return TrainWorker(
world_rank=distributed_context.world_rank,
local_rank=distributed_context.local_rank,
node_rank=distributed_context.node_rank,
actor_id=actor._actor_id.hex(),
node_id=actor_metadata.node_id,
node_ip=actor_metadata.node_ip,
pid=actor_metadata.pid,
gpu_ids=actor_metadata.gpu_ids,
status=ActorStatus.ALIVE,
resources=TrainResources(resources=worker.resources),
log_file_path=worker.log_file_path,
)
workers: List[TrainWorker] = [_convert_worker(worker) for worker in workers]
run_attempt = self._run_attempts[run_id][attempt_id]
run_attempt.status = RunAttemptStatus.RUNNING
run_attempt.status_detail = None
run_attempt.workers = workers
self._create_or_update_train_run_attempt(run_attempt)
def update_train_run_attempt_finished(
self,
run_id: str,
attempt_id: str,
):
run_attempt = self._run_attempts[run_id][attempt_id]
run_attempt.status = RunAttemptStatus.FINISHED
run_attempt.status_detail = None
run_attempt.end_time_ns = current_time_ns()
mark_workers_dead(run_attempt)
# Block to avoid case where controller is dead but attempt is not terminal.
self._create_or_update_train_run_attempt(run_attempt, block=True)
def update_train_run_attempt_errored(
self,
run_id: str,
attempt_id: str,
status_detail: str,
):
run_attempt = self._run_attempts[run_id][attempt_id]
run_attempt.status = RunAttemptStatus.ERRORED
run_attempt.status_detail = status_detail
run_attempt.end_time_ns = current_time_ns()
mark_workers_dead(run_attempt)
# Block to avoid case where controller is dead but attempt is not terminal.
self._create_or_update_train_run_attempt(run_attempt, block=True)
def update_train_run_attempt_aborted(
self,
run_id: str,
attempt_id: str,
):
run_attempt = self._run_attempts[run_id][attempt_id]
update_train_run_attempt_aborted(run_attempt=run_attempt, graceful=True)
# Block to avoid case where controller is dead but attempt is not terminal.
self._create_or_update_train_run_attempt(run_attempt, block=True)
def get_train_run_framework(self, run_id: str) -> Optional[TrainingFramework]:
run = self._runs[run_id]
return run.run_settings.backend_config.framework
def _create_or_update_train_run(
self, run: TrainRun, *, block: bool = False
) -> None:
ref = self._state_actor.create_or_update_train_run.remote(run)
if block:
ray.get(ref)
def _create_or_update_train_run_attempt(
self, run_attempt: TrainRunAttempt, *, block: bool = False
) -> None:
ref = self._state_actor.create_or_update_train_run_attempt.remote(run_attempt)
if block:
ray.get(ref)
def _get_scheduling_status_detail(
num_workers: int, resources_per_worker: Dict[str, float]
) -> str:
return f"Scheduling {num_workers} workers, each requiring: {resources_per_worker}."