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." )