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

404 lines
12 KiB
Python

import os
import time
import uuid
from pathlib import Path
from typing import Dict, List, Optional
from unittest.mock import MagicMock
import ray
from ray.train import BackendConfig, Checkpoint
from ray.train._internal.data_config import DataConfig
from ray.train.backend import Backend
from ray.train.context import TrainContext
from ray.train.v2._internal.execution.context import (
DistributedContext,
TrainRunContext,
)
from ray.train.v2._internal.execution.failure_handling import (
FailureDecision,
FailurePolicy,
)
from ray.train.v2._internal.execution.scaling_policy import (
NoopDecision,
ScalingDecision,
ScalingPolicy,
)
from ray.train.v2._internal.execution.storage import StorageContext
from ray.train.v2._internal.execution.training_report import _TrainingReport
from ray.train.v2._internal.execution.worker_group import (
WorkerGroup,
WorkerGroupContext,
WorkerGroupPollStatus,
WorkerGroupState,
WorkerStatus,
)
from ray.train.v2._internal.execution.worker_group.execution_group import ReplicaGroup
from ray.train.v2._internal.state.schema import (
ActorStatus,
BackendConfig as BackendConfigSchema,
CheckpointConfig as CheckpointConfigSchema,
DataConfig as DataConfigSchema,
DataExecutionOptions,
FailureConfig as FailureConfigSchema,
RunAttemptStatus,
RunConfig as RunConfigSchema,
RunSettings,
RunStatus,
ScalingConfig as ScalingConfigSchema,
TrainResources,
TrainRun,
TrainRunAttempt,
TrainWorker,
)
from ray.train.v2._internal.state.util import execution_options_to_model
from ray.train.v2._internal.util import ObjectRefWrapper, time_monotonic
from ray.train.v2.api.exceptions import TrainingFailedError
from ray.train.v2.api.validation_config import ValidationTaskConfig
class MockReplicaGroupBackend(Backend):
has_replica_groups = True
class MockReplicaGroupBackendConfig(BackendConfig):
@property
def backend_cls(self):
return MockReplicaGroupBackend
@ray.remote
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
def get_count(self):
return self.count
class DummyWorkerGroup(WorkerGroup):
_start_failure = None
_poll_failure = None
# TODO: Clean this up and use Mocks instead.
def __init__(
self,
train_run_context: TrainRunContext,
worker_group_context: WorkerGroupContext,
callbacks=None,
):
self._num_workers = worker_group_context.num_workers
self._worker_group_state = None
self._worker_statuses = {}
self._replaced_replica_groups: List[int] = []
self._replica_groups = None
self._latest_poll_status: Optional[WorkerGroupPollStatus] = None
def poll_status(self, *args, **kwargs) -> WorkerGroupPollStatus:
if self._poll_failure:
raise self._poll_failure
return WorkerGroupPollStatus(
worker_statuses=self._worker_statuses,
)
def _start(self):
num_workers = self._num_workers
if self._start_failure:
raise self._start_failure
workers = [MagicMock() for i in range(num_workers)]
self._worker_group_state = WorkerGroupState(
start_time=time_monotonic(),
workers=workers,
placement_group_handle=MagicMock(),
sync_actor=None,
)
self._worker_statuses = {
i: WorkerStatus(running=True, error=None) for i in range(num_workers)
}
self._replica_groups = [
ReplicaGroup([workers[i]], resources_per_worker={})
for i in range(num_workers)
]
def shutdown(self):
self._worker_group_state = None
def abort(self):
pass
def replace_replica_group(self, replica_group_index: int):
self._replaced_replica_groups.append(replica_group_index)
# === Test methods ===
def clear_worker(self):
for worker_status in self._worker_statuses.values():
worker_status.error = None
worker_status.running = True
def error_worker(self, worker_index):
status = self._worker_statuses[worker_index]
status.error = RuntimeError(f"Worker {worker_index} failed")
def finish_worker(self, worker_index):
status = self._worker_statuses[worker_index]
status.running = False
@classmethod
def set_start_failure(cls, start_failure):
cls._start_failure = start_failure
@classmethod
def set_poll_failure(cls, poll_failure):
cls._poll_failure = poll_failure
class MockScalingPolicy(ScalingPolicy):
def __init__(self, scaling_config):
self._recovery_decision_queue = []
self._monitor_decision_queue = []
super().__init__(scaling_config)
def _get_num_workers_for_resource_request(self) -> int:
return self.scaling_config.num_workers
def make_decision_for_non_running_worker_group(self) -> ScalingDecision:
if self._recovery_decision_queue:
return self._recovery_decision_queue.pop(0)
return NoopDecision()
def make_decision_for_running_worker_group(
self,
worker_group_state: WorkerGroupState,
worker_group_status: WorkerGroupPollStatus,
) -> ScalingDecision:
if self._monitor_decision_queue:
return self._monitor_decision_queue.pop(0)
return NoopDecision()
# === Test methods ===
def queue_recovery_decision(self, decision):
self._recovery_decision_queue.append(decision)
def queue_monitor_decision(self, decision):
self._monitor_decision_queue.append(decision)
class MockFailurePolicy(FailurePolicy):
def __init__(self, failure_config):
self._decision_queue = []
super().__init__(failure_config)
def make_decision(
self, training_failed_error: TrainingFailedError
) -> FailureDecision:
if self._decision_queue:
return self._decision_queue.pop(0)
return FailureDecision.NOOP
# === Test methods ===
def queue_decision(self, decision):
self._decision_queue.append(decision)
class DummyObjectRefWrapper(ObjectRefWrapper):
"""Mock object that returns the object passed in without going through ray.put."""
def __init__(self, obj):
self._obj = obj
def get(self):
return self._obj
_RUN_ID = "mock_run_id"
def create_mock_train_run(
status: RunStatus = RunStatus.RUNNING,
controller_actor_id: Optional[str] = None,
end_time_ns: Optional[int] = None,
id: Optional[str] = None,
status_detail: Optional[str] = None,
train_loop_config: Optional[Dict] = None,
):
return TrainRun(
schema_version=0,
id=id or _RUN_ID,
name="test_run",
job_id=uuid.uuid4().hex,
controller_actor_id=controller_actor_id or uuid.uuid4().hex,
status=status,
status_detail=status_detail,
start_time_ns=time.time_ns(),
end_time_ns=end_time_ns,
controller_log_file_path="/tmp/ray/session_xxx/logs/train/ray-train-app-controller.log",
framework_versions={"ray": ray.__version__},
run_settings=RunSettings(
train_loop_config=train_loop_config,
backend_config=BackendConfigSchema(framework=None, config={}),
scaling_config=ScalingConfigSchema(
num_workers=1,
use_gpu=False,
resources_per_worker=None,
placement_strategy="PACK",
accelerator_type=None,
use_tpu=False,
topology=None,
bundle_label_selector=None,
),
datasets=["dataset_1"],
data_config=DataConfigSchema(
datasets_to_split="all",
data_execution_options=DataExecutionOptions(
default=execution_options_to_model(
DataConfig.default_ingest_options()
),
),
enable_shard_locality=True,
),
run_config=RunConfigSchema(
name="test_run",
failure_config=FailureConfigSchema(
max_failures=0, controller_failure_limit=-1
),
worker_runtime_env={"type": "conda"},
checkpoint_config=CheckpointConfigSchema(
num_to_keep=None,
checkpoint_score_attribute=None,
checkpoint_score_order="max",
),
storage_path="s3://bucket/path",
storage_filesystem=None,
),
),
)
def create_mock_train_run_attempt(
attempt_id: str = "mock_attempt_id",
status: RunAttemptStatus = RunAttemptStatus.RUNNING,
end_time_ns: Optional[int] = None,
run_id: Optional[str] = None,
worker_status: Optional[ActorStatus] = ActorStatus.ALIVE,
status_detail: Optional[str] = None,
):
worker = TrainWorker(
world_rank=0,
local_rank=0,
node_rank=0,
actor_id=uuid.uuid4().hex,
node_id=uuid.uuid4().hex,
node_ip="127.0.0.1",
pid=1234,
gpu_ids=[0],
status=worker_status,
resources=TrainResources(resources={"CPU": 1}),
log_file_path="/tmp/ray/session_xxx/logs/train/ray-train-app-worker.log",
)
return TrainRunAttempt(
schema_version=0,
attempt_id=attempt_id,
run_id=run_id or _RUN_ID,
status=status,
status_detail=status_detail,
start_time_ns=time.time_ns(),
resources=[TrainResources(resources={"CPU": 1})],
workers=[worker],
end_time_ns=end_time_ns,
)
def create_dummy_run_context(**kwargs: dict) -> TrainRunContext:
"""Create a standardized TrainRunContext for testing.
Args:
**kwargs: Optional overrides for the default configuration.
Returns:
TrainRunContext: A standardized TrainRunContext instance for testing.
"""
from ray.train import BackendConfig, DataConfig
from ray.train.v2._internal.execution.context import TrainRunContext
from ray.train.v2.api.config import RunConfig, ScalingConfig
config = dict(
run_config=RunConfig(name="test"),
train_loop_config={},
scaling_config=ScalingConfig(num_workers=1),
backend_config=BackendConfig(),
dataset_config=DataConfig(),
)
config.update(kwargs)
return TrainRunContext(**config)
class DummyTrainContext(TrainContext):
"""A dummy TrainContext subclass for testing."""
def __init__(self):
self.train_run_context = create_dummy_run_context()
self.distributed_context = DistributedContext(
world_rank=0,
world_size=1,
local_rank=0,
local_world_size=1,
node_rank=0,
)
# Mock everything else since we don't need the actual functionality
self.execution_context = MagicMock()
self.storage_context = MagicMock()
self.dataset_shards = {}
def get_run_config(self):
return self.train_run_context.run_config
def create_dummy_train_context() -> TrainContext:
"""Create a standardized TrainContext for testing.
Returns:
TrainContext: A standardized TrainContext instance for testing.
"""
return DummyTrainContext()
def create_dummy_training_reports(
num_results: int,
storage_context: StorageContext,
include_metrics: bool = True,
include_validation: bool = False,
starting_checkpoint_index: int = 0,
) -> List[_TrainingReport]:
training_results = []
for i in range(num_results):
metrics = {"score": i} if include_metrics else {}
validation = (
ValidationTaskConfig(fn_kwargs={"arg": i}) if include_validation else False
)
checkpoint_path = os.path.join(
storage_context.experiment_fs_path,
f"checkpoint_{starting_checkpoint_index + i}",
)
os.makedirs(checkpoint_path, exist_ok=True)
training_results.append(
_TrainingReport(
checkpoint=Checkpoint(
path=Path(checkpoint_path).as_posix(),
filesystem=storage_context.storage_filesystem,
),
metrics=metrics,
validation=validation,
)
)
return training_results