chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import logging
|
||||
|
||||
from google.protobuf.struct_pb2 import Struct
|
||||
|
||||
from ray.core.generated.export_train_state_pb2 import (
|
||||
ExportTrainRunAttemptEventData as ProtoTrainRunAttempt,
|
||||
ExportTrainRunEventData as ProtoTrainRun,
|
||||
)
|
||||
from ray.dashboard.modules.metrics.dashboards.common import Panel
|
||||
from ray.dashboard.modules.metrics.dashboards.train_dashboard_panels import (
|
||||
TRAIN_RUN_PANELS,
|
||||
TRAIN_WORKER_PANELS,
|
||||
)
|
||||
from ray.train.v2._internal.state.schema import (
|
||||
ActorStatus,
|
||||
BackendConfig,
|
||||
DataConfig,
|
||||
ExecutionOptions,
|
||||
RunAttemptStatus,
|
||||
RunConfig,
|
||||
RunSettings,
|
||||
RunStatus,
|
||||
ScalingConfig,
|
||||
TrainRun,
|
||||
TrainRunAttempt,
|
||||
TrainWorker,
|
||||
)
|
||||
from ray.train.v2._internal.util import TrainingFramework
|
||||
|
||||
# Increment each time the exported Train schema changes (proto, pydantic, or
|
||||
# exported json) so downstream consumers can distinguish schema versions.
|
||||
TRAIN_SCHEMA_VERSION = 4
|
||||
RAY_TRAIN_VERSION = 2
|
||||
|
||||
# Status mapping dictionaries
|
||||
_ACTOR_STATUS_MAP = {
|
||||
ActorStatus.ALIVE: ProtoTrainRunAttempt.ActorStatus.ALIVE,
|
||||
ActorStatus.DEAD: ProtoTrainRunAttempt.ActorStatus.DEAD,
|
||||
}
|
||||
|
||||
_RUN_ATTEMPT_STATUS_MAP = {
|
||||
RunAttemptStatus.PENDING: ProtoTrainRunAttempt.RunAttemptStatus.PENDING,
|
||||
RunAttemptStatus.RUNNING: ProtoTrainRunAttempt.RunAttemptStatus.RUNNING,
|
||||
RunAttemptStatus.FINISHED: ProtoTrainRunAttempt.RunAttemptStatus.FINISHED,
|
||||
RunAttemptStatus.ERRORED: ProtoTrainRunAttempt.RunAttemptStatus.ERRORED,
|
||||
RunAttemptStatus.ABORTED: ProtoTrainRunAttempt.RunAttemptStatus.ABORTED,
|
||||
}
|
||||
|
||||
_RUN_STATUS_MAP = {
|
||||
RunStatus.INITIALIZING: ProtoTrainRun.RunStatus.INITIALIZING,
|
||||
RunStatus.SCHEDULING: ProtoTrainRun.RunStatus.SCHEDULING,
|
||||
RunStatus.RUNNING: ProtoTrainRun.RunStatus.RUNNING,
|
||||
RunStatus.RESTARTING: ProtoTrainRun.RunStatus.RESTARTING,
|
||||
RunStatus.RESIZING: ProtoTrainRun.RunStatus.RESIZING,
|
||||
RunStatus.FINISHED: ProtoTrainRun.RunStatus.FINISHED,
|
||||
RunStatus.ERRORED: ProtoTrainRun.RunStatus.ERRORED,
|
||||
RunStatus.ABORTED: ProtoTrainRun.RunStatus.ABORTED,
|
||||
}
|
||||
|
||||
_TRAINING_FRAMEWORK_MAP = {
|
||||
None: ProtoTrainRun.BackendConfig.TrainingFramework.TRAINING_FRAMEWORK_UNSPECIFIED,
|
||||
TrainingFramework.TORCH: ProtoTrainRun.BackendConfig.TrainingFramework.TORCH,
|
||||
TrainingFramework.JAX: ProtoTrainRun.BackendConfig.TrainingFramework.JAX,
|
||||
TrainingFramework.TENSORFLOW: ProtoTrainRun.BackendConfig.TrainingFramework.TENSORFLOW,
|
||||
TrainingFramework.XGBOOST: ProtoTrainRun.BackendConfig.TrainingFramework.XGBOOST,
|
||||
TrainingFramework.LIGHTGBM: ProtoTrainRun.BackendConfig.TrainingFramework.LIGHTGBM,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _dict_to_struct(d: dict) -> Struct:
|
||||
"""Returns a protobuf Struct from a dictionary."""
|
||||
s = Struct()
|
||||
s.update(d)
|
||||
return s
|
||||
|
||||
|
||||
def _to_proto_resources(resources: dict) -> ProtoTrainRunAttempt.TrainResources:
|
||||
"""Convert resources dictionary to protobuf TrainResources."""
|
||||
return ProtoTrainRunAttempt.TrainResources(resources=resources)
|
||||
|
||||
|
||||
def _to_proto_worker(worker: TrainWorker) -> ProtoTrainRunAttempt.TrainWorker:
|
||||
"""Convert TrainWorker to protobuf format."""
|
||||
status = None
|
||||
if worker.status is not None:
|
||||
status = _ACTOR_STATUS_MAP[worker.status]
|
||||
|
||||
return ProtoTrainRunAttempt.TrainWorker(
|
||||
world_rank=worker.world_rank,
|
||||
local_rank=worker.local_rank,
|
||||
node_rank=worker.node_rank,
|
||||
actor_id=bytes.fromhex(worker.actor_id),
|
||||
node_id=bytes.fromhex(worker.node_id),
|
||||
node_ip=worker.node_ip,
|
||||
pid=worker.pid,
|
||||
gpu_ids=worker.gpu_ids,
|
||||
status=status,
|
||||
resources=_to_proto_resources(worker.resources.resources),
|
||||
log_file_path=worker.log_file_path,
|
||||
)
|
||||
|
||||
|
||||
# Main conversion functions
|
||||
def train_run_attempt_to_proto(attempt: TrainRunAttempt) -> ProtoTrainRunAttempt:
|
||||
"""Convert TrainRunAttempt to protobuf format."""
|
||||
proto_attempt = ProtoTrainRunAttempt(
|
||||
schema_version=TRAIN_SCHEMA_VERSION,
|
||||
ray_train_version=RAY_TRAIN_VERSION,
|
||||
run_id=attempt.run_id,
|
||||
attempt_id=attempt.attempt_id,
|
||||
status=_RUN_ATTEMPT_STATUS_MAP[attempt.status],
|
||||
status_detail=attempt.status_detail,
|
||||
start_time_ns=attempt.start_time_ns,
|
||||
end_time_ns=attempt.end_time_ns,
|
||||
resources=[_to_proto_resources(r.resources) for r in attempt.resources],
|
||||
workers=[_to_proto_worker(w) for w in attempt.workers],
|
||||
)
|
||||
|
||||
return proto_attempt
|
||||
|
||||
|
||||
def _to_proto_dashboard_panel(panel: Panel) -> ProtoTrainRun.DashboardPanelMetadata:
|
||||
"""Convert Dashboard Panel to protobuf format."""
|
||||
proto_panel = ProtoTrainRun.DashboardPanelMetadata(
|
||||
id=str(panel.id),
|
||||
title=panel.title,
|
||||
)
|
||||
|
||||
return proto_panel
|
||||
|
||||
|
||||
def to_proto_backend_config(
|
||||
backend_config: BackendConfig,
|
||||
) -> ProtoTrainRun.BackendConfig:
|
||||
"""Convert BackendConfig to protobuf format."""
|
||||
proto_backend_config = ProtoTrainRun.BackendConfig(
|
||||
framework=_TRAINING_FRAMEWORK_MAP[backend_config.framework],
|
||||
)
|
||||
|
||||
proto_backend_config.config.CopyFrom(_dict_to_struct(backend_config.config))
|
||||
|
||||
return proto_backend_config
|
||||
|
||||
|
||||
def to_proto_scaling_config(
|
||||
scaling_config: ScalingConfig,
|
||||
) -> ProtoTrainRun.ScalingConfig:
|
||||
"""Convert ScalingConfig to protobuf format."""
|
||||
proto_scaling_config = ProtoTrainRun.ScalingConfig(
|
||||
use_gpu=scaling_config.use_gpu,
|
||||
placement_strategy=scaling_config.placement_strategy,
|
||||
use_tpu=scaling_config.use_tpu,
|
||||
)
|
||||
|
||||
if isinstance(scaling_config.num_workers, tuple):
|
||||
proto_scaling_config.num_workers_range.CopyFrom(
|
||||
ProtoTrainRun.ScalingConfig.IntRange(
|
||||
min=scaling_config.num_workers[0],
|
||||
max=scaling_config.num_workers[1],
|
||||
)
|
||||
)
|
||||
else:
|
||||
proto_scaling_config.num_workers_fixed = scaling_config.num_workers
|
||||
|
||||
if scaling_config.resources_per_worker is not None:
|
||||
proto_scaling_config.resources_per_worker.values.update(
|
||||
scaling_config.resources_per_worker
|
||||
)
|
||||
|
||||
if scaling_config.accelerator_type is not None:
|
||||
proto_scaling_config.accelerator_type = scaling_config.accelerator_type
|
||||
if scaling_config.topology is not None:
|
||||
proto_scaling_config.topology = scaling_config.topology
|
||||
|
||||
if scaling_config.bundle_label_selector is not None:
|
||||
selectors = scaling_config.bundle_label_selector
|
||||
if isinstance(selectors, dict):
|
||||
proto_scaling_config.label_selector_single.values.update(selectors)
|
||||
else:
|
||||
proto_scaling_config.label_selector_list.values.extend(
|
||||
[ProtoTrainRun.ScalingConfig.StringMap(values=s) for s in selectors]
|
||||
)
|
||||
|
||||
return proto_scaling_config
|
||||
|
||||
|
||||
def _to_proto_execution_options(
|
||||
execution_options: ExecutionOptions,
|
||||
) -> ProtoTrainRun.ExecutionOptions:
|
||||
"""Convert a single ExecutionOptions schema model to protobuf."""
|
||||
return ProtoTrainRun.ExecutionOptions(
|
||||
resource_limits=_dict_to_struct(execution_options.resource_limits),
|
||||
exclude_resources=_dict_to_struct(execution_options.exclude_resources),
|
||||
preserve_order=execution_options.preserve_order,
|
||||
actor_locality_enabled=execution_options.actor_locality_enabled,
|
||||
verbose_progress=execution_options.verbose_progress,
|
||||
)
|
||||
|
||||
|
||||
def to_proto_data_config(data_config: DataConfig) -> ProtoTrainRun.DataConfig:
|
||||
"""Convert DataConfig to protobuf format."""
|
||||
data_execution_options = data_config.data_execution_options
|
||||
proto_data_config = ProtoTrainRun.DataConfig(
|
||||
enable_shard_locality=data_config.enable_shard_locality,
|
||||
data_execution_options=ProtoTrainRun.DataExecutionOptions(
|
||||
default=_to_proto_execution_options(data_execution_options.default),
|
||||
per_dataset_execution_options={
|
||||
name: _to_proto_execution_options(opts)
|
||||
for name, opts in data_execution_options.per_dataset_execution_options.items()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if data_config.datasets_to_split == "all":
|
||||
proto_data_config.all.SetInParent()
|
||||
else:
|
||||
proto_data_config.datasets.values.extend(data_config.datasets_to_split)
|
||||
|
||||
return proto_data_config
|
||||
|
||||
|
||||
def _to_proto_failure_config(run_config: RunConfig) -> ProtoTrainRun.FailureConfig:
|
||||
"""Convert RunConfig.failure_config to protobuf format."""
|
||||
return ProtoTrainRun.FailureConfig(
|
||||
max_failures=run_config.failure_config.max_failures,
|
||||
controller_failure_limit=run_config.failure_config.controller_failure_limit,
|
||||
)
|
||||
|
||||
|
||||
def _to_proto_checkpoint_config(
|
||||
run_config: RunConfig,
|
||||
) -> ProtoTrainRun.CheckpointConfig:
|
||||
"""Convert RunConfig.checkpoint_config to protobuf format."""
|
||||
checkpoint_score_order = ProtoTrainRun.CheckpointConfig.CheckpointScoreOrder.Value(
|
||||
run_config.checkpoint_config.checkpoint_score_order.upper()
|
||||
)
|
||||
|
||||
proto_checkpoint_config = ProtoTrainRun.CheckpointConfig(
|
||||
checkpoint_score_order=checkpoint_score_order
|
||||
)
|
||||
if run_config.checkpoint_config.num_to_keep is not None:
|
||||
proto_checkpoint_config.num_to_keep = run_config.checkpoint_config.num_to_keep
|
||||
if run_config.checkpoint_config.checkpoint_score_attribute is not None:
|
||||
proto_checkpoint_config.checkpoint_score_attribute = (
|
||||
run_config.checkpoint_config.checkpoint_score_attribute
|
||||
)
|
||||
return proto_checkpoint_config
|
||||
|
||||
|
||||
def to_proto_run_config(run_config: RunConfig) -> ProtoTrainRun.RunConfig:
|
||||
"""Convert RunConfig to protobuf format."""
|
||||
proto_run_config = ProtoTrainRun.RunConfig(
|
||||
name=run_config.name,
|
||||
failure_config=_to_proto_failure_config(run_config),
|
||||
worker_runtime_env=_dict_to_struct(run_config.worker_runtime_env),
|
||||
checkpoint_config=_to_proto_checkpoint_config(run_config),
|
||||
storage_path=run_config.storage_path,
|
||||
)
|
||||
|
||||
if run_config.storage_filesystem is not None:
|
||||
proto_run_config.storage_filesystem = run_config.storage_filesystem
|
||||
|
||||
return proto_run_config
|
||||
|
||||
|
||||
def _to_proto_run_settings(run_settings: RunSettings) -> ProtoTrainRun.RunSettings:
|
||||
"""Convert RunSettings to protobuf format."""
|
||||
|
||||
proto_run_settings = ProtoTrainRun.RunSettings(
|
||||
backend_config=to_proto_backend_config(run_settings.backend_config),
|
||||
scaling_config=to_proto_scaling_config(run_settings.scaling_config),
|
||||
datasets=run_settings.datasets,
|
||||
data_config=to_proto_data_config(run_settings.data_config),
|
||||
run_config=to_proto_run_config(run_settings.run_config),
|
||||
)
|
||||
|
||||
if run_settings.train_loop_config is not None:
|
||||
proto_run_settings.train_loop_config.CopyFrom(
|
||||
_dict_to_struct(run_settings.train_loop_config)
|
||||
)
|
||||
|
||||
return proto_run_settings
|
||||
|
||||
|
||||
def train_run_to_proto(run: TrainRun) -> ProtoTrainRun:
|
||||
"""Convert TrainRun to protobuf format."""
|
||||
|
||||
proto_train_run_panels = [_to_proto_dashboard_panel(p) for p in TRAIN_RUN_PANELS]
|
||||
proto_train_worker_panels = [
|
||||
_to_proto_dashboard_panel(p) for p in TRAIN_WORKER_PANELS
|
||||
]
|
||||
|
||||
proto_train_run = ProtoTrainRun(
|
||||
schema_version=TRAIN_SCHEMA_VERSION,
|
||||
ray_train_version=RAY_TRAIN_VERSION,
|
||||
id=run.id,
|
||||
name=run.name,
|
||||
job_id=bytes.fromhex(run.job_id),
|
||||
controller_actor_id=bytes.fromhex(run.controller_actor_id),
|
||||
status=_RUN_STATUS_MAP[run.status],
|
||||
status_detail=run.status_detail,
|
||||
start_time_ns=run.start_time_ns,
|
||||
end_time_ns=run.end_time_ns,
|
||||
controller_log_file_path=run.controller_log_file_path,
|
||||
train_run_panels=proto_train_run_panels,
|
||||
train_worker_panels=proto_train_worker_panels,
|
||||
framework_versions=run.framework_versions,
|
||||
run_settings=_to_proto_run_settings(run.run_settings),
|
||||
)
|
||||
|
||||
return proto_train_run
|
||||
@@ -0,0 +1,532 @@
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
from pydantic import field_validator
|
||||
|
||||
from ray._common.pydantic_compat import BaseModel, Field
|
||||
from ray.dashboard.modules.job.pydantic_models import JobDetails
|
||||
from ray.train.v2._internal.util import TrainingFramework
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
MAX_ERROR_STACK_TRACE_LENGTH = 50000
|
||||
|
||||
|
||||
def _to_json_serializable_value(value: Any, *, max_depth: int = 3) -> Any:
|
||||
"""Recursively coerce a value into a human-readable, JSON serializable representation.
|
||||
|
||||
If ``value`` is a list or dict, this function walks through it and replaces non-JSON
|
||||
serializable fields (e.g. custom objects, modules, tensors, callables, etc.) with a
|
||||
human-readable string representation.
|
||||
|
||||
Args:
|
||||
value: Any Python value. Primitives pass through; collections recurse;
|
||||
other types are stringified.
|
||||
max_depth: Truncates dicts nested beyond ``max_depth`` to ``"..."``.
|
||||
Lists do not consume depth.
|
||||
|
||||
Returns:
|
||||
The JSON serializable representation of the value.
|
||||
"""
|
||||
if max_depth <= 0:
|
||||
raise ValueError("max_depth must be greater than 0")
|
||||
|
||||
def _safe_str(v):
|
||||
try:
|
||||
return str(v)
|
||||
except Exception:
|
||||
return type(v).__name__
|
||||
|
||||
def _walk(value, depth):
|
||||
if value is None or isinstance(value, (bool, int, str)):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return str(value) if not math.isfinite(value) else value
|
||||
if isinstance(value, Mapping):
|
||||
if depth <= 0:
|
||||
return "..."
|
||||
try:
|
||||
items = list(value.items())
|
||||
except Exception:
|
||||
# Custom Mapping subclass with a broken `.items()`.
|
||||
return type(value).__name__
|
||||
return {_safe_str(k): _walk(v, depth - 1) for k, v in items}
|
||||
|
||||
# Tuples, sets, and frozensets all become lists in JSON.
|
||||
if isinstance(value, (list, tuple, set, frozenset)):
|
||||
return [_walk(v, depth) for v in value]
|
||||
|
||||
cls = type(value)
|
||||
# Use class name if no custom string representation is defined.
|
||||
if cls.__str__ is object.__str__ and cls.__repr__ is object.__repr__:
|
||||
return cls.__name__
|
||||
return _safe_str(value)
|
||||
|
||||
return _walk(value, max_depth)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class RunStatus(str, Enum):
|
||||
"""Enumeration of the possible statuses for a Train run."""
|
||||
|
||||
# ====== Active States ======
|
||||
# The Train run is currently in the process of initializing.
|
||||
INITIALIZING = "INITIALIZING"
|
||||
# The Train run is waiting to be scheduled.
|
||||
SCHEDULING = "SCHEDULING"
|
||||
# The Train run is currently in progress.
|
||||
RUNNING = "RUNNING"
|
||||
# The Train run is recovering from a failure or restart.
|
||||
RESTARTING = "RESTARTING"
|
||||
# The Train run is resizing.
|
||||
RESIZING = "RESIZING"
|
||||
|
||||
# ===== Terminal States ======
|
||||
# The Train run completed successfully.
|
||||
FINISHED = "FINISHED"
|
||||
# The Train run failed due to an error in the training workers.
|
||||
ERRORED = "ERRORED"
|
||||
# The Train run was terminated due to system or controller errors.
|
||||
ABORTED = "ABORTED"
|
||||
|
||||
def is_terminal(self) -> bool:
|
||||
return self in [RunStatus.FINISHED, RunStatus.ERRORED, RunStatus.ABORTED]
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class RunAttemptStatus(str, Enum):
|
||||
"""Enumeration of the possible statuses for a Train run attempt."""
|
||||
|
||||
# ====== Active States ======
|
||||
# The run attempt is waiting to be scheduled.
|
||||
PENDING = "PENDING"
|
||||
# The run attempt is currently in progress.
|
||||
RUNNING = "RUNNING"
|
||||
|
||||
# ===== Terminal States =====
|
||||
# The run attempt completed successfully.
|
||||
FINISHED = "FINISHED"
|
||||
# The run attempt failed due to an error in the training workers.
|
||||
ERRORED = "ERRORED"
|
||||
# The run attempt was terminated due to system or controller errors.
|
||||
ABORTED = "ABORTED"
|
||||
|
||||
def is_terminal(self) -> bool:
|
||||
return self in [
|
||||
RunAttemptStatus.FINISHED,
|
||||
RunAttemptStatus.ERRORED,
|
||||
RunAttemptStatus.ABORTED,
|
||||
]
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ActorStatus(str, Enum):
|
||||
"""Enumeration of the statuses for a Train worker actor."""
|
||||
|
||||
# The actor is currently active.
|
||||
ALIVE = "ALIVE"
|
||||
# The actor is no longer active.
|
||||
DEAD = "DEAD"
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainResources(BaseModel):
|
||||
"""Resources allocated for a Train worker or run."""
|
||||
|
||||
resources: Dict[str, float] = Field(
|
||||
description="A dictionary specifying the types and amounts of resources "
|
||||
"allocated (e.g., CPU, GPU)."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainWorker(BaseModel):
|
||||
"""Metadata about a Ray Train worker."""
|
||||
|
||||
world_rank: int = Field(
|
||||
description="The global rank of the worker in the training cluster."
|
||||
)
|
||||
local_rank: int = Field(description="The local rank of the worker on its node.")
|
||||
node_rank: int = Field(description="The rank of the worker's node in the cluster.")
|
||||
actor_id: str = Field(description="The unique ID of the worker's actor.")
|
||||
node_id: str = Field(
|
||||
description="The unique ID of the node where the worker is running."
|
||||
)
|
||||
node_ip: str = Field(
|
||||
description="The IP address of the node where the worker is running."
|
||||
)
|
||||
pid: int = Field(description="The process ID of the worker.")
|
||||
gpu_ids: List[int] = Field(description="A list of GPU IDs allocated to the worker.")
|
||||
status: Optional[ActorStatus] = Field(
|
||||
None, description="The current status of the worker actor."
|
||||
)
|
||||
resources: TrainResources = Field(
|
||||
description="The resources allocated to this Train worker."
|
||||
)
|
||||
log_file_path: Optional[str] = Field(
|
||||
None, description="The path to the log file for the Train worker."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class MemoryInfo(BaseModel):
|
||||
"""Memory usage information for a process."""
|
||||
|
||||
rss: int = Field(description="The resident set size (RSS) memory usage in bytes.")
|
||||
vms: int = Field(description="The virtual memory size (VMS) usage in bytes.")
|
||||
pfaults: Optional[int] = Field(None, description="The number of page faults.")
|
||||
pageins: Optional[int] = Field(None, description="The number of page-ins.")
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ProcessStats(BaseModel):
|
||||
"""CPU and memory statistics for a process."""
|
||||
|
||||
cpuPercent: float = Field(description="The percentage of CPU usage.")
|
||||
mem: Optional[List[int]] = Field(
|
||||
None,
|
||||
description="Memory statistics, including total memory, free memory, "
|
||||
"and memory usage ratio.",
|
||||
)
|
||||
memoryInfo: MemoryInfo = Field(description="Detailed memory usage information.")
|
||||
|
||||
|
||||
class ProcessGPUUsage(BaseModel):
|
||||
"""GPU usage statistics for a process."""
|
||||
|
||||
pid: int = Field(description="The process ID.")
|
||||
gpuMemoryUsage: int = Field(description="The GPU memory usage in bytes.")
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class GPUStats(BaseModel):
|
||||
"""Statistics for a GPU."""
|
||||
|
||||
uuid: str = Field(description="The unique identifier of the GPU.")
|
||||
index: int = Field(description="The index of the GPU.")
|
||||
name: str = Field(description="The name of the GPU.")
|
||||
utilizationGpu: Optional[float] = Field(
|
||||
None, description="The percentage utilization of the GPU."
|
||||
)
|
||||
memoryUsed: float = Field(description="The amount of GPU memory used in bytes.")
|
||||
memoryTotal: float = Field(description="The total amount of GPU memory in bytes.")
|
||||
processInfo: ProcessGPUUsage = Field(
|
||||
description="GPU usage statistics for the associated process."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class DecoratedTrainWorker(TrainWorker):
|
||||
"""Detailed metadata for a Ray Train worker, including process and GPU stats."""
|
||||
|
||||
processStats: Optional[ProcessStats] = Field(
|
||||
None, description="CPU and memory statistics for the worker process."
|
||||
)
|
||||
gpus: List[GPUStats] = Field(
|
||||
default_factory=list,
|
||||
description="A list of GPUs used by the worker process,"
|
||||
" with detailed statistics.",
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRunAttempt(BaseModel):
|
||||
"""Metadata for an individual attempt to execute a Train run."""
|
||||
|
||||
run_id: str = Field(description="Unique identifier for the parent Train run.")
|
||||
attempt_id: str = Field(
|
||||
description="Unique identifier for this specific Train run attempt."
|
||||
)
|
||||
status: RunAttemptStatus = Field(
|
||||
description="The current execution status of the Train run attempt."
|
||||
)
|
||||
status_detail: Optional[str] = Field(
|
||||
None,
|
||||
description="Additional details about the status,"
|
||||
" including error messages if applicable.",
|
||||
)
|
||||
start_time_ns: int = Field(
|
||||
description="The UNIX timestamp (in nanoseconds)"
|
||||
" when the Train run attempt started."
|
||||
)
|
||||
end_time_ns: Optional[int] = Field(
|
||||
None,
|
||||
description="The UNIX timestamp (in nanoseconds)"
|
||||
" when the Train run attempt ended. "
|
||||
"If null, the attempt is still ongoing.",
|
||||
)
|
||||
resources: List[TrainResources] = Field(
|
||||
description="The resources (e.g., CPU, GPU) allocated to the Train run attempt."
|
||||
)
|
||||
workers: List[TrainWorker] = Field(
|
||||
description="List of Train workers participating in this attempt, "
|
||||
"sorted by global ranks."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class DecoratedTrainRunAttempt(TrainRunAttempt):
|
||||
"""Detailed metadata for a Train run attempt, including decorated worker data."""
|
||||
|
||||
workers: List[DecoratedTrainWorker] = Field(
|
||||
description="A list of Train workers with detailed statistics, "
|
||||
"sorted by global ranks."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ExecutionOptions(BaseModel):
|
||||
"""ExecutionOptions for a single Ray Data ingest pipeline."""
|
||||
|
||||
resource_limits: Dict[str, Any] = Field(
|
||||
description="The resource limits applied to the Ray Data execution plan."
|
||||
)
|
||||
exclude_resources: Dict[str, Any] = Field(
|
||||
description="The resources excluded from the Ray Data execution plan "
|
||||
"(e.g. resources reserved by Ray Train workers)."
|
||||
)
|
||||
|
||||
@field_validator("resource_limits", "exclude_resources", mode="before")
|
||||
@classmethod
|
||||
def _sanitize_dict(cls, v):
|
||||
return _to_json_serializable_value(v)
|
||||
|
||||
preserve_order: bool = Field(
|
||||
description="Whether to preserve the order of outputs across operators."
|
||||
)
|
||||
actor_locality_enabled: bool = Field(
|
||||
description="Whether actor-based locality optimizations are enabled."
|
||||
)
|
||||
verbose_progress: bool = Field(
|
||||
description="Whether verbose progress reporting is enabled."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class DataExecutionOptions(BaseModel):
|
||||
"""ExecutionOptions for a Ray Train run, split into defaults and per-dataset overrides."""
|
||||
|
||||
default: ExecutionOptions = Field(
|
||||
description="Execution options applied to any dataset without a per-dataset override."
|
||||
)
|
||||
per_dataset_execution_options: Dict[str, ExecutionOptions] = Field(
|
||||
default_factory=dict,
|
||||
description="Per-dataset execution option overrides, keyed by dataset name.",
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class DataConfig(BaseModel):
|
||||
"""Configuration for dataset splitting and execution options within Ray Train."""
|
||||
|
||||
datasets_to_split: Union[Literal["all"], List[str]] = Field(
|
||||
description="Which datasets to split; either 'all' or a list of dataset names."
|
||||
)
|
||||
execution_options: Optional[Dict] = Field(
|
||||
default=None,
|
||||
deprecated="DEPRECATED: Use data_execution_options instead.",
|
||||
)
|
||||
data_execution_options: DataExecutionOptions = Field(
|
||||
description="Data execution options"
|
||||
)
|
||||
enable_shard_locality: bool = Field(
|
||||
description="Whether to enable shard locality optimization."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ScalingConfig(BaseModel):
|
||||
"""Scaling config for a Train run."""
|
||||
|
||||
num_workers: Union[int, Tuple[int, int]] = Field(
|
||||
description="The number of workers for the Train run."
|
||||
)
|
||||
use_gpu: bool = Field(description="Whether to use GPUs for the Train run.")
|
||||
resources_per_worker: Optional[Dict[str, float]] = Field(
|
||||
None, description="The resources per worker for a Train run."
|
||||
)
|
||||
placement_strategy: str = Field(
|
||||
description="The placement strategy for the Train run."
|
||||
)
|
||||
accelerator_type: Optional[str] = Field(
|
||||
None, description="The accelerator type for the Train run."
|
||||
)
|
||||
use_tpu: bool = Field(description="Whether to use TPUs for the Train run.")
|
||||
topology: Optional[str] = Field(None, description="The topology for the Train run.")
|
||||
bundle_label_selector: Optional[
|
||||
Union[Dict[str, str], List[Dict[str, str]]]
|
||||
] = Field(None, description="The bundle label selector for the Train run.")
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class FailureConfig(BaseModel):
|
||||
"""Failure config for a Train run."""
|
||||
|
||||
max_failures: int = Field(
|
||||
description="The maximum number of failures for a Train run."
|
||||
)
|
||||
controller_failure_limit: int = Field(
|
||||
description="The maximum number of controller failures to tolerate."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class CheckpointConfig(BaseModel):
|
||||
"""Checkpoint config for a Train run."""
|
||||
|
||||
num_to_keep: Optional[int] = Field(
|
||||
None,
|
||||
description="The number of most recent checkpoints to keep. Older checkpoints may be deleted.",
|
||||
)
|
||||
checkpoint_score_attribute: Optional[str] = Field(
|
||||
None,
|
||||
description="Attribute used to score and rank checkpoints; can be a metric key or attribute.",
|
||||
)
|
||||
checkpoint_score_order: Literal["max", "min"] = Field(
|
||||
description="Order to rank checkpoint scores, 'max' for higher-is-better, 'min' for lower-is-better.",
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class RunConfig(BaseModel):
|
||||
"""Run configuration parameters for a Train run, encompassing failure,
|
||||
runtime environment, checkpoint settings, and storage path."""
|
||||
|
||||
name: str = Field(description="The name of the Train run.")
|
||||
failure_config: FailureConfig = Field(
|
||||
description="The failure config for a Train run."
|
||||
)
|
||||
worker_runtime_env: Dict[str, Any] = Field(
|
||||
description="The worker runtime env for a Train run."
|
||||
)
|
||||
|
||||
@field_validator("worker_runtime_env", mode="before")
|
||||
@classmethod
|
||||
def _sanitize_worker_runtime_env(cls, v):
|
||||
return _to_json_serializable_value(v)
|
||||
|
||||
checkpoint_config: CheckpointConfig = Field(
|
||||
description="The checkpoint config for a Train run."
|
||||
)
|
||||
storage_path: str = Field(description="The storage path for a Train run.")
|
||||
storage_filesystem: Optional[str] = Field(
|
||||
None, description="The storage filesystem for a Train run."
|
||||
)
|
||||
|
||||
@field_validator("storage_filesystem", mode="before")
|
||||
@classmethod
|
||||
def _sanitize_storage_filesystem(cls, v):
|
||||
return _to_json_serializable_value(v)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class BackendConfig(BaseModel):
|
||||
"""Backend config for a Train run."""
|
||||
|
||||
framework: Optional[TrainingFramework] = Field(
|
||||
None, description="The training framework for this backend config."
|
||||
)
|
||||
config: Dict[str, Any] = Field(
|
||||
description="Training framework-specific configuration fields."
|
||||
)
|
||||
|
||||
@field_validator("config", mode="before")
|
||||
@classmethod
|
||||
def _sanitize_config(cls, v):
|
||||
return _to_json_serializable_value(v)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class RunSettings(BaseModel):
|
||||
"""Settings for a Train run, primarily consisting of configs set before a train run starts.
|
||||
|
||||
This includes the train loop config, backend config, scaling config, dataset configs,
|
||||
and runtime configuration.
|
||||
"""
|
||||
|
||||
train_loop_config: Optional[Dict] = Field(
|
||||
None, description="The user defined train loop config for a Train run."
|
||||
)
|
||||
|
||||
@field_validator("train_loop_config", mode="before")
|
||||
@classmethod
|
||||
def _sanitize_train_loop_config(cls, v):
|
||||
return _to_json_serializable_value(v)
|
||||
|
||||
backend_config: BackendConfig = Field(
|
||||
description="The backend config for a Train run. Can vary with the framework (e.g. TorchConfig)"
|
||||
)
|
||||
scaling_config: ScalingConfig = Field(
|
||||
description="The scaling config for this Train run."
|
||||
)
|
||||
datasets: List[str] = Field(
|
||||
description="A list of dataset names for a Train run.",
|
||||
)
|
||||
data_config: DataConfig = Field(
|
||||
description="The data config for a Train run.",
|
||||
)
|
||||
run_config: RunConfig = Field(
|
||||
description="Run configuration for this Train run, including failure, runtime environment, checkpoint settings, and storage path."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRun(BaseModel):
|
||||
"""Metadata for a Ray Train run, including its details and status."""
|
||||
|
||||
id: str = Field(description="Unique identifier for the Train run.")
|
||||
name: str = Field(description="Human-readable name assigned to the Train run.")
|
||||
job_id: str = Field(description="The Ray Job ID associated with this Train run.")
|
||||
controller_actor_id: str = Field(
|
||||
description="Unique ID of the actor managing the Train run."
|
||||
)
|
||||
status: RunStatus = Field(
|
||||
description="The current execution status of the Train run."
|
||||
)
|
||||
status_detail: Optional[str] = Field(
|
||||
None,
|
||||
description="Additional details about the current status, "
|
||||
"including error messages if applicable.",
|
||||
)
|
||||
start_time_ns: int = Field(
|
||||
description="The UNIX timestamp (in nanoseconds) when the Train run started."
|
||||
)
|
||||
end_time_ns: Optional[int] = Field(
|
||||
None,
|
||||
description="The UNIX timestamp (in nanoseconds) when the Train run ended. "
|
||||
"If null, the run is still in progress.",
|
||||
)
|
||||
controller_log_file_path: Optional[str] = Field(
|
||||
None, description="The path to the log file for the Train run controller."
|
||||
)
|
||||
framework_versions: Dict[str, str] = Field(
|
||||
description="The relevant framework versions for this Train run,"
|
||||
"including the Ray version and training framework version."
|
||||
)
|
||||
run_settings: RunSettings = Field(
|
||||
description="The run settings for this Train run, including train loop config, "
|
||||
"backend config, scaling config, dataset details, and runtime configuration."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class DecoratedTrainRun(TrainRun):
|
||||
"""Detailed metadata for a Ray Train run, including attempts and job details."""
|
||||
|
||||
attempts: List[DecoratedTrainRunAttempt] = Field(
|
||||
description="A list of attempts made to execute the Train run."
|
||||
)
|
||||
job_details: Optional[JobDetails] = Field(
|
||||
None,
|
||||
description="Detailed information about the job that initiated this Train run.",
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRunsResponse(BaseModel):
|
||||
"""Response containing a list of decorated Train runs."""
|
||||
|
||||
train_runs: List[DecoratedTrainRun] = Field(
|
||||
description="A list of Train runs with detailed metadata."
|
||||
)
|
||||
@@ -0,0 +1,301 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict, defaultdict
|
||||
from typing import Dict, Optional
|
||||
|
||||
import ray
|
||||
from ray._private import ray_constants
|
||||
from ray._private.event.export_event_logger import (
|
||||
EventLogType,
|
||||
check_export_api_enabled,
|
||||
get_export_event_logger,
|
||||
)
|
||||
from ray.actor import ActorHandle
|
||||
from ray.train.v2._internal.constants import (
|
||||
CONTROLLERS_TO_POLL_PER_ITERATION,
|
||||
DEFAULT_ENABLE_STATE_ACTOR_RECONCILIATION,
|
||||
DEFAULT_STATE_ACTOR_RECONCILIATION_INTERVAL_S,
|
||||
ENABLE_STATE_ACTOR_RECONCILIATION_ENV_VAR,
|
||||
GET_ACTOR_TIMEOUT_S,
|
||||
STATE_ACTOR_RECONCILIATION_INTERVAL_S_ENV_VAR,
|
||||
)
|
||||
from ray.train.v2._internal.state.schema import (
|
||||
TrainRun,
|
||||
TrainRunAttempt,
|
||||
)
|
||||
from ray.train.v2._internal.state.util import (
|
||||
is_actor_alive,
|
||||
update_train_run_aborted,
|
||||
update_train_run_attempt_aborted,
|
||||
)
|
||||
from ray.train.v2._internal.util import time_monotonic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrainStateActor:
|
||||
def __init__(
|
||||
self,
|
||||
# TODO: group into single config if we need to do similar polling elsewhere
|
||||
enable_state_actor_reconciliation: bool = False,
|
||||
reconciliation_interval_s: float = 30,
|
||||
get_actor_timeout_s: int = GET_ACTOR_TIMEOUT_S,
|
||||
controllers_to_poll_per_iteration: int = CONTROLLERS_TO_POLL_PER_ITERATION,
|
||||
):
|
||||
# NOTE: All runs and attempts are stored in memory.
|
||||
# This may be a memory issue for large runs.
|
||||
# TODO: consider cleaning up runs over time.
|
||||
self._runs: Dict[str, TrainRun] = OrderedDict()
|
||||
# {run_id: {attempt_id: TrainRunAttempt}}
|
||||
self._run_attempts: Dict[str, OrderedDict[str, TrainRunAttempt]] = defaultdict(
|
||||
OrderedDict
|
||||
)
|
||||
(
|
||||
self._export_logger,
|
||||
self._is_train_run_export_api_enabled,
|
||||
self._is_train_run_attempt_export_api_enabled,
|
||||
) = self._init_export_logger()
|
||||
|
||||
# TODO: consider row level locking if loop takes too long.
|
||||
self._runs_lock = threading.RLock()
|
||||
self._run_attempts_lock = threading.RLock()
|
||||
|
||||
# Set env vars related to reconciling train run/attempt state.
|
||||
if enable_state_actor_reconciliation:
|
||||
self._reconciliation_interval_s = reconciliation_interval_s
|
||||
self._controllers_to_poll_per_iteration = controllers_to_poll_per_iteration
|
||||
self._get_actor_timeout_s = get_actor_timeout_s
|
||||
self._start_run_state_reconciliation_thread()
|
||||
|
||||
def _abort_live_runs_with_dead_controllers(
|
||||
self, last_poll_run_id: Optional[str]
|
||||
) -> str:
|
||||
aborted_run_ids = []
|
||||
with self._runs_lock:
|
||||
runs = list(self._runs.values())
|
||||
|
||||
# Start iterating from poll index.
|
||||
starting_poll_index = 0
|
||||
if last_poll_run_id is not None:
|
||||
for poll_index, run in enumerate(runs):
|
||||
if run.id == last_poll_run_id:
|
||||
starting_poll_index = (poll_index + 1) % len(runs)
|
||||
break
|
||||
|
||||
# Abort runs.
|
||||
num_polled_runs = 0
|
||||
poll_index = starting_poll_index
|
||||
while (
|
||||
poll_index < starting_poll_index + len(runs)
|
||||
and num_polled_runs < self._controllers_to_poll_per_iteration
|
||||
):
|
||||
run = runs[poll_index % len(runs)]
|
||||
poll_index += 1
|
||||
last_poll_run_id = run.id
|
||||
if run.status.is_terminal():
|
||||
continue
|
||||
try:
|
||||
if not is_actor_alive(
|
||||
run.controller_actor_id, self._get_actor_timeout_s
|
||||
):
|
||||
update_train_run_aborted(run, False)
|
||||
self.create_or_update_train_run(run)
|
||||
aborted_run_ids.append(run.id)
|
||||
except ray.util.state.exception.RayStateApiException:
|
||||
logger.exception(
|
||||
"State API unavailable when checking if actor is alive. "
|
||||
"Will check again on next poll."
|
||||
)
|
||||
num_polled_runs += 1
|
||||
|
||||
# Abort run attempts.
|
||||
with self._run_attempts_lock:
|
||||
for run_id in aborted_run_ids:
|
||||
latest_run_attempt = self._get_latest_run_attempt(run_id)
|
||||
if latest_run_attempt and not latest_run_attempt.status.is_terminal():
|
||||
update_train_run_attempt_aborted(latest_run_attempt, False)
|
||||
self.create_or_update_train_run_attempt(latest_run_attempt)
|
||||
|
||||
return last_poll_run_id
|
||||
|
||||
def _start_run_state_reconciliation_thread(self) -> None:
|
||||
def _reconciliation_loop():
|
||||
last_poll_run_id = None
|
||||
latest_poll_time = float("-inf")
|
||||
while True:
|
||||
# Wait for the poll interval to elapse.
|
||||
time_since_last_poll = time_monotonic() - latest_poll_time
|
||||
if time_since_last_poll < self._reconciliation_interval_s:
|
||||
remaining_time = (
|
||||
self._reconciliation_interval_s - time_since_last_poll
|
||||
)
|
||||
time.sleep(remaining_time)
|
||||
|
||||
last_poll_run_id = self._abort_live_runs_with_dead_controllers(
|
||||
last_poll_run_id
|
||||
)
|
||||
latest_poll_time = time_monotonic()
|
||||
|
||||
threading.Thread(target=_reconciliation_loop, daemon=True).start()
|
||||
|
||||
def _get_latest_run_attempt(self, run_id: str) -> Optional[TrainRunAttempt]:
|
||||
with self._run_attempts_lock:
|
||||
# NOTE: run_attempts is OrderedDict from attempt_id to TrainRunAttempt.
|
||||
run_attempts = self._run_attempts.get(run_id, {})
|
||||
if not run_attempts:
|
||||
return None
|
||||
return next(reversed(run_attempts.values()))
|
||||
|
||||
def create_or_update_train_run(self, run: TrainRun) -> None:
|
||||
with self._runs_lock:
|
||||
self._runs[run.id] = run
|
||||
run_copy = copy.deepcopy(run)
|
||||
self._maybe_export_train_run(run_copy)
|
||||
|
||||
def create_or_update_train_run_attempt(self, run_attempt: TrainRunAttempt) -> None:
|
||||
with self._run_attempts_lock:
|
||||
self._run_attempts[run_attempt.run_id][run_attempt.attempt_id] = run_attempt
|
||||
run_attempt_copy = copy.deepcopy(run_attempt)
|
||||
self._maybe_export_train_run_attempt(run_attempt_copy)
|
||||
|
||||
def get_train_runs(self) -> Dict[str, TrainRun]:
|
||||
with self._runs_lock:
|
||||
return self._runs
|
||||
|
||||
def get_train_run_attempts(self) -> Dict[str, Dict[str, TrainRunAttempt]]:
|
||||
with self._run_attempts_lock:
|
||||
return self._run_attempts
|
||||
|
||||
# ============================
|
||||
# Export API
|
||||
# ============================
|
||||
|
||||
def is_export_api_enabled(self) -> bool:
|
||||
return self._export_logger is not None
|
||||
|
||||
def _init_export_logger(self) -> tuple[Optional[logging.Logger], bool, bool]:
|
||||
"""Initialize the export logger and check if the export API is enabled.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
- The export logger (or None if export API is not enabled).
|
||||
- A boolean indicating if the export API is enabled for train runs.
|
||||
- A boolean indicating if the export API is enabled for train run attempts.
|
||||
"""
|
||||
# Proto schemas should be imported within the scope of TrainStateActor to
|
||||
# prevent serialization errors.
|
||||
from ray.core.generated.export_event_pb2 import ExportEvent
|
||||
|
||||
is_train_run_export_api_enabled = check_export_api_enabled(
|
||||
ExportEvent.SourceType.EXPORT_TRAIN_RUN
|
||||
)
|
||||
is_train_run_attempt_export_api_enabled = check_export_api_enabled(
|
||||
ExportEvent.SourceType.EXPORT_TRAIN_RUN_ATTEMPT
|
||||
)
|
||||
export_api_enabled = (
|
||||
is_train_run_export_api_enabled or is_train_run_attempt_export_api_enabled
|
||||
)
|
||||
|
||||
if not export_api_enabled:
|
||||
return None, False, False
|
||||
|
||||
log_directory = os.path.join(
|
||||
ray._private.worker._global_node.get_session_dir_path(), "logs"
|
||||
)
|
||||
logger = None
|
||||
try:
|
||||
logger = get_export_event_logger(
|
||||
EventLogType.TRAIN_STATE,
|
||||
log_directory,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Unable to initialize the export event logger, so no Train export "
|
||||
"events will be written."
|
||||
)
|
||||
|
||||
if logger is None:
|
||||
return None, False, False
|
||||
|
||||
return (
|
||||
logger,
|
||||
is_train_run_export_api_enabled,
|
||||
is_train_run_attempt_export_api_enabled,
|
||||
)
|
||||
|
||||
def _maybe_export_train_run(self, run: TrainRun) -> None:
|
||||
if not self._is_train_run_export_api_enabled:
|
||||
return
|
||||
|
||||
from ray.train.v2._internal.state.export import train_run_to_proto
|
||||
|
||||
run_proto = train_run_to_proto(run)
|
||||
self._export_logger.send_event(run_proto)
|
||||
|
||||
def _maybe_export_train_run_attempt(self, run_attempt: TrainRunAttempt) -> None:
|
||||
if not self._is_train_run_attempt_export_api_enabled:
|
||||
return
|
||||
|
||||
from ray.train.v2._internal.state.export import train_run_attempt_to_proto
|
||||
|
||||
run_attempt_proto = train_run_attempt_to_proto(run_attempt)
|
||||
self._export_logger.send_event(run_attempt_proto)
|
||||
|
||||
|
||||
TRAIN_STATE_ACTOR_NAME = "train_v2_state_actor"
|
||||
TRAIN_STATE_ACTOR_NAMESPACE = "_train_state_actor"
|
||||
|
||||
_state_actor_lock: threading.RLock = threading.RLock()
|
||||
|
||||
|
||||
def get_or_create_state_actor() -> ActorHandle:
|
||||
"""Get or create the Ray Train state actor singleton.
|
||||
|
||||
This is a long-living, detached actor living on the head node
|
||||
that gets initialized when the first Train run happens on the
|
||||
Ray cluster.
|
||||
"""
|
||||
with _state_actor_lock:
|
||||
state_actor = (
|
||||
ray.remote(TrainStateActor)
|
||||
.options(
|
||||
num_cpus=0,
|
||||
name=TRAIN_STATE_ACTOR_NAME,
|
||||
namespace=TRAIN_STATE_ACTOR_NAMESPACE,
|
||||
get_if_exists=True,
|
||||
lifetime="detached",
|
||||
resources={"node:__internal_head__": 0.001},
|
||||
# Escape from the parent's placement group
|
||||
scheduling_strategy="DEFAULT",
|
||||
max_restarts=-1,
|
||||
max_task_retries=-1,
|
||||
)
|
||||
.remote(
|
||||
enable_state_actor_reconciliation=ray_constants.env_bool(
|
||||
ENABLE_STATE_ACTOR_RECONCILIATION_ENV_VAR,
|
||||
DEFAULT_ENABLE_STATE_ACTOR_RECONCILIATION,
|
||||
),
|
||||
reconciliation_interval_s=float(
|
||||
os.getenv(
|
||||
STATE_ACTOR_RECONCILIATION_INTERVAL_S_ENV_VAR,
|
||||
DEFAULT_STATE_ACTOR_RECONCILIATION_INTERVAL_S,
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return state_actor
|
||||
|
||||
|
||||
def get_state_actor() -> Optional[ActorHandle]:
|
||||
"""Get the `TrainStateActor` if exists, otherwise return None."""
|
||||
try:
|
||||
return ray.get_actor(
|
||||
name=TRAIN_STATE_ACTOR_NAME,
|
||||
namespace=TRAIN_STATE_ACTOR_NAMESPACE,
|
||||
)
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -0,0 +1,326 @@
|
||||
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}."
|
||||
@@ -0,0 +1,97 @@
|
||||
import time
|
||||
|
||||
from ray.data._internal.execution.interfaces.execution_options import ExecutionOptions
|
||||
from ray.train._internal.data_config import DataConfig
|
||||
from ray.train.v2._internal.state.schema import (
|
||||
ActorStatus,
|
||||
DataConfig as DataConfigSchema,
|
||||
DataExecutionOptions,
|
||||
ExecutionOptions as ExecutionOptionsSchema,
|
||||
RunAttemptStatus,
|
||||
RunStatus,
|
||||
TrainRun,
|
||||
TrainRunAttempt,
|
||||
)
|
||||
from ray.util.state import get_actor
|
||||
|
||||
_GRACEFUL_ABORT_STATUS_DETAIL = "Run aborted due to user interrupt (SIGINT)."
|
||||
_DEAD_CONTROLLER_ABORT_STATUS_DETAIL = (
|
||||
"Run aborted because the driver process exited unexpectedly."
|
||||
)
|
||||
|
||||
|
||||
def update_train_run_aborted(run: TrainRun, graceful: bool) -> None:
|
||||
run.status = RunStatus.ABORTED
|
||||
if graceful:
|
||||
run.status_detail = _GRACEFUL_ABORT_STATUS_DETAIL
|
||||
else:
|
||||
run.status_detail = _DEAD_CONTROLLER_ABORT_STATUS_DETAIL
|
||||
run.end_time_ns = current_time_ns()
|
||||
|
||||
|
||||
def update_train_run_attempt_aborted(
|
||||
run_attempt: TrainRunAttempt, graceful: bool
|
||||
) -> None:
|
||||
if graceful:
|
||||
run_attempt.status_detail = _GRACEFUL_ABORT_STATUS_DETAIL
|
||||
else:
|
||||
run_attempt.status_detail = _DEAD_CONTROLLER_ABORT_STATUS_DETAIL
|
||||
run_attempt.status = RunAttemptStatus.ABORTED
|
||||
run_attempt.end_time_ns = current_time_ns()
|
||||
mark_workers_dead(run_attempt)
|
||||
|
||||
|
||||
def mark_workers_dead(run_attempt: TrainRunAttempt) -> None:
|
||||
for worker in run_attempt.workers:
|
||||
worker.status = ActorStatus.DEAD
|
||||
|
||||
|
||||
def current_time_ns() -> int:
|
||||
return time.time_ns()
|
||||
|
||||
|
||||
def is_actor_alive(actor_id: str, timeout: int) -> bool:
|
||||
"""Returns whether actor is alive."""
|
||||
actor_state = get_actor(actor_id, timeout=timeout)
|
||||
return actor_state and actor_state.state != "DEAD"
|
||||
|
||||
|
||||
def construct_data_config(data_config: DataConfig) -> DataConfigSchema:
|
||||
"""Serialize a user-facing DataConfig into the exportable schema.
|
||||
|
||||
Note: This function assumes data_config._execution_options (a defaultdict)
|
||||
hasn't been read between initialization of the field and this function call.
|
||||
Any read materializes a dataset key and affects the data config shape,
|
||||
wrongly capturing a per dataset execution options even if the user only
|
||||
provided a default.
|
||||
"""
|
||||
exec_options = data_config._execution_options
|
||||
|
||||
per_dataset_execution_options = {}
|
||||
if exec_options:
|
||||
per_dataset_execution_options = {
|
||||
ds_name: execution_options_to_model(opts)
|
||||
for ds_name, opts in exec_options.items()
|
||||
}
|
||||
|
||||
return DataConfigSchema(
|
||||
datasets_to_split=data_config._datasets_to_split,
|
||||
data_execution_options=DataExecutionOptions(
|
||||
default=execution_options_to_model(exec_options.default_factory()),
|
||||
per_dataset_execution_options=per_dataset_execution_options,
|
||||
),
|
||||
enable_shard_locality=data_config._enable_shard_locality,
|
||||
)
|
||||
|
||||
|
||||
def execution_options_to_model(
|
||||
execution_options: ExecutionOptions,
|
||||
) -> ExecutionOptionsSchema:
|
||||
"""Convert a ray.data ExecutionOptions object into the export schema model."""
|
||||
return ExecutionOptionsSchema(
|
||||
resource_limits=execution_options.resource_limits.to_resource_dict(),
|
||||
exclude_resources=execution_options.exclude_resources.to_resource_dict(),
|
||||
preserve_order=execution_options.preserve_order,
|
||||
actor_locality_enabled=execution_options.actor_locality_enabled,
|
||||
verbose_progress=execution_options.verbose_progress,
|
||||
)
|
||||
Reference in New Issue
Block a user