chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
from ray.train._internal.state.state_manager import TrainRunStateManager
|
||||
|
||||
try:
|
||||
import pydantic # noqa: F401
|
||||
except ImportError:
|
||||
raise ModuleNotFoundError(
|
||||
"pydantic isn't installed."
|
||||
"To install pydantic, please run 'pip install pydantic'"
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TrainRunStateManager",
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
from typing import Optional
|
||||
|
||||
from ray.core.generated.export_train_state_pb2 import (
|
||||
ExportTrainRunAttemptEventData as ProtoTrainRunAttempt,
|
||||
ExportTrainRunEventData as ProtoTrainRun,
|
||||
)
|
||||
from ray.train._internal.state.schema import (
|
||||
ActorStatusEnum,
|
||||
RunStatusEnum,
|
||||
TrainRunInfo,
|
||||
TrainWorkerInfo,
|
||||
)
|
||||
|
||||
TRAIN_SCHEMA_VERSION = 1
|
||||
RAY_TRAIN_VERSION = 1
|
||||
|
||||
# Status mapping dictionaries
|
||||
_ACTOR_STATUS_MAP = {
|
||||
ActorStatusEnum.ALIVE: ProtoTrainRunAttempt.ActorStatus.ALIVE,
|
||||
ActorStatusEnum.DEAD: ProtoTrainRunAttempt.ActorStatus.DEAD,
|
||||
}
|
||||
|
||||
_RUN_ATTEMPT_STATUS_MAP = {
|
||||
RunStatusEnum.STARTED: ProtoTrainRunAttempt.RunAttemptStatus.PENDING,
|
||||
RunStatusEnum.RUNNING: ProtoTrainRunAttempt.RunAttemptStatus.RUNNING,
|
||||
RunStatusEnum.FINISHED: ProtoTrainRunAttempt.RunAttemptStatus.FINISHED,
|
||||
RunStatusEnum.ERRORED: ProtoTrainRunAttempt.RunAttemptStatus.ERRORED,
|
||||
RunStatusEnum.ABORTED: ProtoTrainRunAttempt.RunAttemptStatus.ABORTED,
|
||||
}
|
||||
|
||||
_RUN_STATUS_MAP = {
|
||||
RunStatusEnum.STARTED: ProtoTrainRun.RunStatus.INITIALIZING,
|
||||
RunStatusEnum.RUNNING: ProtoTrainRun.RunStatus.RUNNING,
|
||||
RunStatusEnum.FINISHED: ProtoTrainRun.RunStatus.FINISHED,
|
||||
RunStatusEnum.ERRORED: ProtoTrainRun.RunStatus.ERRORED,
|
||||
RunStatusEnum.ABORTED: ProtoTrainRun.RunStatus.ABORTED,
|
||||
}
|
||||
|
||||
|
||||
def _ms_to_ns(ms: Optional[int]) -> Optional[int]:
|
||||
if ms is None:
|
||||
return None
|
||||
return ms * 1000000
|
||||
|
||||
|
||||
# Helper conversion functions
|
||||
def _to_proto_resources(resources: dict) -> ProtoTrainRunAttempt.TrainResources:
|
||||
"""Convert resources dictionary to protobuf TrainResources."""
|
||||
return ProtoTrainRunAttempt.TrainResources(resources=resources)
|
||||
|
||||
|
||||
def _to_proto_worker(worker: TrainWorkerInfo) -> ProtoTrainRunAttempt.TrainWorker:
|
||||
"""Convert TrainWorker to protobuf format."""
|
||||
proto_worker = 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=_ACTOR_STATUS_MAP[worker.status],
|
||||
resources=_to_proto_resources(worker.resources),
|
||||
)
|
||||
|
||||
return proto_worker
|
||||
|
||||
|
||||
# Main conversion functions
|
||||
def train_run_info_to_proto_run(run_info: TrainRunInfo) -> ProtoTrainRun:
|
||||
"""Convert TrainRunInfo to TrainRun protobuf format."""
|
||||
proto_run = ProtoTrainRun(
|
||||
schema_version=TRAIN_SCHEMA_VERSION,
|
||||
ray_train_version=RAY_TRAIN_VERSION,
|
||||
id=run_info.id,
|
||||
name=run_info.name,
|
||||
job_id=bytes.fromhex(run_info.job_id),
|
||||
controller_actor_id=bytes.fromhex(run_info.controller_actor_id),
|
||||
status=_RUN_STATUS_MAP[run_info.run_status],
|
||||
status_detail=run_info.status_detail,
|
||||
start_time_ns=_ms_to_ns(run_info.start_time_ms),
|
||||
end_time_ns=_ms_to_ns(run_info.end_time_ms),
|
||||
)
|
||||
|
||||
return proto_run
|
||||
|
||||
|
||||
def train_run_info_to_proto_attempt(run_info: TrainRunInfo) -> ProtoTrainRunAttempt:
|
||||
"""Convert TrainRunInfo to TrainRunAttempt protobuf format."""
|
||||
|
||||
proto_attempt = ProtoTrainRunAttempt(
|
||||
schema_version=TRAIN_SCHEMA_VERSION,
|
||||
ray_train_version=RAY_TRAIN_VERSION,
|
||||
run_id=run_info.id,
|
||||
attempt_id=run_info.id, # Same as run_id
|
||||
status=_RUN_ATTEMPT_STATUS_MAP[run_info.run_status],
|
||||
status_detail=run_info.status_detail,
|
||||
start_time_ns=_ms_to_ns(run_info.start_time_ms),
|
||||
end_time_ns=_ms_to_ns(run_info.end_time_ms),
|
||||
resources=[_to_proto_resources(r) for r in run_info.resources],
|
||||
workers=[_to_proto_worker(worker) for worker in run_info.workers],
|
||||
)
|
||||
|
||||
return proto_attempt
|
||||
@@ -0,0 +1,165 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ray._common.pydantic_compat import BaseModel, Field
|
||||
from ray.dashboard.modules.job.pydantic_models import JobDetails
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
MAX_ERROR_STACK_TRACE_LENGTH = 50000
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class RunStatusEnum(str, Enum):
|
||||
"""Enumeration for the status of a train run."""
|
||||
|
||||
# (Deprecated) Replaced by RUNNING.
|
||||
# The train run has started
|
||||
STARTED = "STARTED"
|
||||
# The train run is running
|
||||
RUNNING = "RUNNING"
|
||||
# The train run was terminated as expected
|
||||
FINISHED = "FINISHED"
|
||||
# The train run was terminated early due to errors in the training function
|
||||
ERRORED = "ERRORED"
|
||||
# The train run was terminated early due to system errors or controller errors
|
||||
ABORTED = "ABORTED"
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ActorStatusEnum(str, Enum):
|
||||
DEAD = "DEAD"
|
||||
ALIVE = "ALIVE"
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainWorkerInfo(BaseModel):
|
||||
"""Metadata of a Ray Train worker."""
|
||||
|
||||
actor_id: str = Field(description="Actor ID of the worker.")
|
||||
world_rank: int = Field(description="World rank of the worker.")
|
||||
local_rank: int = Field(description="Local rank of the worker.")
|
||||
node_rank: int = Field(description="Node rank of the worker.")
|
||||
node_id: str = Field(description="ID of the node that the worker is running on.")
|
||||
node_ip: str = Field(
|
||||
description="IP address of the node that the worker is running on."
|
||||
)
|
||||
pid: int = Field(description="Process ID of the worker.")
|
||||
gpu_ids: List[int] = Field(
|
||||
description="A list of GPU ids allocated to that worker."
|
||||
)
|
||||
status: ActorStatusEnum = Field(
|
||||
description="The status of the train worker actor. It can be ALIVE or DEAD."
|
||||
)
|
||||
resources: Dict[str, float] = Field(
|
||||
description="The resources allocated to the worker."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class MemoryInfo(BaseModel):
|
||||
rss: int
|
||||
vms: int
|
||||
pfaults: Optional[int] = None
|
||||
pageins: Optional[int] = None
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ProcessStats(BaseModel):
|
||||
cpuPercent: float
|
||||
# total memory, free memory, memory used ratio
|
||||
mem: Optional[List[int]] = None
|
||||
memoryInfo: MemoryInfo
|
||||
|
||||
|
||||
class ProcessGPUUsage(BaseModel):
|
||||
# This gpu usage stats from a process
|
||||
pid: int
|
||||
gpuMemoryUsage: int
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class GPUStats(BaseModel):
|
||||
uuid: str
|
||||
index: int
|
||||
name: str
|
||||
utilizationGpu: Optional[float] = None
|
||||
memoryUsed: float
|
||||
memoryTotal: float
|
||||
processInfo: ProcessGPUUsage
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainWorkerInfoWithDetails(TrainWorkerInfo):
|
||||
"""Metadata of a Ray Train worker."""
|
||||
|
||||
processStats: Optional[ProcessStats] = Field(
|
||||
None, description="Process stats of the worker."
|
||||
)
|
||||
gpus: List[GPUStats] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"GPU stats of the worker. "
|
||||
"Only returns GPUs that are attached to the worker process."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainDatasetInfo(BaseModel):
|
||||
name: str = Field(
|
||||
description="The key of the dataset dict specified in Ray Train Trainer."
|
||||
)
|
||||
dataset_uuid: str = Field(description="The uuid of the dataset.")
|
||||
dataset_name: Optional[str] = Field(None, description="The name of the dataset.")
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRunInfo(BaseModel):
|
||||
"""Metadata for a Ray Train run and information about its workers."""
|
||||
|
||||
name: str = Field(description="The name of the Train run.")
|
||||
id: str = Field(description="The unique identifier for each Train run.")
|
||||
job_id: str = Field(description="The Ray Job ID.")
|
||||
controller_actor_id: str = Field(description="Actor Id of the Train controller.")
|
||||
workers: List[TrainWorkerInfo] = Field(
|
||||
description="A List of Train workers sorted by global ranks."
|
||||
)
|
||||
datasets: List[TrainDatasetInfo] = Field(
|
||||
description="A List of dataset info for this Train run."
|
||||
)
|
||||
run_status: RunStatusEnum = Field(
|
||||
description="The current status of the train run. It can be one of the "
|
||||
"following: RUNNING, FINISHED, ERRORED, or ABORTED."
|
||||
)
|
||||
status_detail: str = Field(
|
||||
description="Detailed information about the current run status, "
|
||||
"such as error messages."
|
||||
)
|
||||
start_time_ms: int = Field(
|
||||
description="The UNIX timestamp of the start time of this Train run."
|
||||
)
|
||||
end_time_ms: Optional[int] = Field(
|
||||
None,
|
||||
description="The UNIX timestamp of the end time of this Train run. "
|
||||
"If null, the Train run has not ended yet.",
|
||||
)
|
||||
resources: List[Dict[str, float]] = Field(
|
||||
description="The resources allocated to the worker."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRunInfoWithDetails(TrainRunInfo):
|
||||
"""Metadata for a Ray Train run and information about its workers."""
|
||||
|
||||
workers: List[TrainWorkerInfoWithDetails] = Field(
|
||||
description="A List of Train workers sorted by global ranks."
|
||||
)
|
||||
job_details: Optional[JobDetails] = Field(
|
||||
None, description="Details of the job that started this Train run."
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class TrainRunsResponse(BaseModel):
|
||||
train_runs: List[TrainRunInfoWithDetails]
|
||||
@@ -0,0 +1,152 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Dict, Optional
|
||||
|
||||
import ray
|
||||
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._internal.state.schema import TrainRunInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ray.remote(num_cpus=0)
|
||||
class TrainStateActor:
|
||||
def __init__(self):
|
||||
self._run_infos: Dict[str, TrainRunInfo] = {}
|
||||
|
||||
(
|
||||
self._export_logger,
|
||||
self._is_train_run_export_api_enabled,
|
||||
self._is_train_run_attempt_export_api_enabled,
|
||||
) = self._init_export_logger()
|
||||
|
||||
def register_train_run(self, run_info: TrainRunInfo) -> None:
|
||||
# Register a new train run.
|
||||
self._run_infos[run_info.id] = run_info
|
||||
|
||||
self._maybe_export_train_run(run_info)
|
||||
self._maybe_export_train_run_attempt(run_info)
|
||||
|
||||
def get_train_run(self, run_id: str) -> Optional[TrainRunInfo]:
|
||||
# Retrieve a registered run with its id
|
||||
return self._run_infos.get(run_id, None)
|
||||
|
||||
def get_all_train_runs(self) -> Dict[str, TrainRunInfo]:
|
||||
# Retrieve all registered train runs
|
||||
return self._run_infos
|
||||
|
||||
# ============================
|
||||
# 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_info: TrainRunInfo) -> None:
|
||||
if not self._is_train_run_export_api_enabled:
|
||||
return
|
||||
|
||||
from ray.train._internal.state.export import train_run_info_to_proto_run
|
||||
|
||||
run_proto = train_run_info_to_proto_run(run_info)
|
||||
self._export_logger.send_event(run_proto)
|
||||
|
||||
def _maybe_export_train_run_attempt(self, run_info: TrainRunInfo) -> None:
|
||||
if not self._is_train_run_attempt_export_api_enabled:
|
||||
return
|
||||
|
||||
from ray.train._internal.state.export import train_run_info_to_proto_attempt
|
||||
|
||||
run_attempt_proto = train_run_info_to_proto_attempt(run_info)
|
||||
self._export_logger.send_event(run_attempt_proto)
|
||||
|
||||
|
||||
TRAIN_STATE_ACTOR_NAME = "train_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 a `TrainStateActor` on the head node."""
|
||||
with _state_actor_lock:
|
||||
state_actor = TrainStateActor.options(
|
||||
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",
|
||||
).remote()
|
||||
|
||||
# Ensure the state actor is ready
|
||||
ray.get(state_actor.__ray_ready__.remote())
|
||||
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,132 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List
|
||||
|
||||
import ray
|
||||
from ray.train._internal.state.schema import (
|
||||
ActorStatusEnum,
|
||||
RunStatusEnum,
|
||||
TrainDatasetInfo,
|
||||
TrainRunInfo,
|
||||
TrainWorkerInfo,
|
||||
)
|
||||
from ray.train._internal.utils import check_for_failure
|
||||
from ray.train._internal.worker_group import WorkerGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data import Dataset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrainRunStateManager:
|
||||
"""A class that aggregates and reports train run info to TrainStateActor.
|
||||
|
||||
This manager class is created on the train controller layer for each run.
|
||||
"""
|
||||
|
||||
def __init__(self, state_actor) -> None:
|
||||
self.state_actor = state_actor
|
||||
self.train_run_info_dict = defaultdict(dict)
|
||||
|
||||
def register_train_run(
|
||||
self,
|
||||
run_id: str,
|
||||
job_id: str,
|
||||
run_name: str,
|
||||
run_status: str,
|
||||
controller_actor_id: str,
|
||||
datasets: Dict[str, "Dataset"],
|
||||
worker_group: WorkerGroup,
|
||||
start_time_ms: float,
|
||||
resources: List[Dict[str, float]],
|
||||
status_detail: str = "",
|
||||
) -> None:
|
||||
"""Collect Train Run Info and report to StateActor."""
|
||||
|
||||
if not self.state_actor:
|
||||
logger.warning(
|
||||
"Unable to register train run since `TrainStateActor` is not started."
|
||||
)
|
||||
return
|
||||
|
||||
def collect_train_worker_info():
|
||||
train_context = ray.train.get_context()
|
||||
core_context = ray.runtime_context.get_runtime_context()
|
||||
return TrainWorkerInfo(
|
||||
world_rank=train_context.get_world_rank(),
|
||||
local_rank=train_context.get_local_rank(),
|
||||
node_rank=train_context.get_node_rank(),
|
||||
actor_id=core_context.get_actor_id(),
|
||||
node_id=core_context.get_node_id(),
|
||||
node_ip=ray.util.get_node_ip_address(),
|
||||
gpu_ids=ray.get_gpu_ids(),
|
||||
pid=os.getpid(),
|
||||
resources=resources[0],
|
||||
status=ActorStatusEnum.ALIVE,
|
||||
)
|
||||
|
||||
futures = [
|
||||
worker_group.execute_single_async(index, collect_train_worker_info)
|
||||
for index in range(len(worker_group))
|
||||
]
|
||||
success, exception = check_for_failure(futures)
|
||||
|
||||
if not success:
|
||||
logger.error(
|
||||
"Failed to collect run information from the Ray Train "
|
||||
f"workers:\n{exception}"
|
||||
)
|
||||
return
|
||||
|
||||
worker_info_list = ray.get(futures)
|
||||
worker_info_list = sorted(worker_info_list, key=lambda info: info.world_rank)
|
||||
|
||||
dataset_info_list = [
|
||||
TrainDatasetInfo(
|
||||
name=ds_name,
|
||||
dataset_name=ds._dataset_name,
|
||||
dataset_uuid=ds._uuid,
|
||||
)
|
||||
for ds_name, ds in datasets.items()
|
||||
]
|
||||
|
||||
updates = dict(
|
||||
id=run_id,
|
||||
job_id=job_id,
|
||||
name=run_name,
|
||||
controller_actor_id=controller_actor_id,
|
||||
workers=worker_info_list,
|
||||
datasets=dataset_info_list,
|
||||
start_time_ms=start_time_ms,
|
||||
run_status=run_status,
|
||||
status_detail=status_detail,
|
||||
resources=resources,
|
||||
)
|
||||
|
||||
# Clear the cached info to avoid registering the same run twice
|
||||
self.train_run_info_dict[run_id] = {}
|
||||
self._update_train_run_info(run_id, updates)
|
||||
|
||||
def end_train_run(
|
||||
self,
|
||||
run_id: str,
|
||||
run_status: RunStatusEnum,
|
||||
status_detail: str,
|
||||
end_time_ms: int,
|
||||
):
|
||||
"""Update the train run status when the training is finished."""
|
||||
updates = dict(
|
||||
run_status=run_status,
|
||||
status_detail=status_detail,
|
||||
end_time_ms=end_time_ms,
|
||||
)
|
||||
self._update_train_run_info(run_id, updates)
|
||||
|
||||
def _update_train_run_info(self, run_id: str, updates: Dict[str, Any]) -> None:
|
||||
"""Update specific fields of a registered TrainRunInfo instance."""
|
||||
if run_id in self.train_run_info_dict:
|
||||
self.train_run_info_dict[run_id].update(updates)
|
||||
train_run_info = TrainRunInfo(**self.train_run_info_dict[run_id])
|
||||
ray.get(self.state_actor.register_train_run.remote(train_run_info))
|
||||
Reference in New Issue
Block a user