352 lines
11 KiB
Python
352 lines
11 KiB
Python
import json
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray.cluster_utils import Cluster
|
|
from ray.train import RunConfig, ScalingConfig
|
|
from ray.train._internal.state.schema import (
|
|
ActorStatusEnum,
|
|
RunStatusEnum,
|
|
TrainDatasetInfo,
|
|
TrainRunInfo,
|
|
TrainWorkerInfo,
|
|
)
|
|
from ray.train._internal.state.state_actor import (
|
|
TRAIN_STATE_ACTOR_NAME,
|
|
TRAIN_STATE_ACTOR_NAMESPACE,
|
|
get_or_create_state_actor,
|
|
)
|
|
from ray.train._internal.state.state_manager import TrainRunStateManager
|
|
from ray.train._internal.worker_group import WorkerGroup
|
|
from ray.train.data_parallel_trainer import DataParallelTrainer
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_gpu_cluster():
|
|
cluster = Cluster()
|
|
cluster.add_node(num_gpus=8, num_cpus=9)
|
|
|
|
ray.shutdown()
|
|
ray.init(
|
|
address=cluster.address,
|
|
runtime_env={"env_vars": {"RAY_TRAIN_ENABLE_STATE_TRACKING": "1"}},
|
|
ignore_reinit_error=True,
|
|
)
|
|
|
|
yield
|
|
|
|
ray.shutdown()
|
|
cluster.shutdown()
|
|
|
|
|
|
RUN_INFO_JSON_SAMPLE = """{
|
|
"name": "default_run",
|
|
"id": "ad5256bc64c04c83833a8b006f531799",
|
|
"job_id": "0000000001",
|
|
"controller_actor_id": "3abd1972a19148d78acc78dd9414736e",
|
|
"start_time_ms": 1717448423000,
|
|
"run_status": "RUNNING",
|
|
"status_detail": "",
|
|
"end_time_ms": null,
|
|
"resources": [{"CPU": 1}, {"CPU": 1}],
|
|
"workers": [
|
|
{
|
|
"actor_id": "3d86c25634a71832dac32c8802000000",
|
|
"world_rank": 0,
|
|
"local_rank": 0,
|
|
"node_rank": 0,
|
|
"node_id": "b1e6cbed8533ae2def4e7e7ced9d19858ceb1ed8ab9ba81ab9c07825",
|
|
"node_ip": "10.0.208.100",
|
|
"pid": 76071,
|
|
"gpu_ids": [0],
|
|
"status": "ALIVE",
|
|
"resources": {"CPU": 1}
|
|
},
|
|
{
|
|
"actor_id": "8f162dd8365346d1b5c98ebd7338c4f9",
|
|
"world_rank": 1,
|
|
"local_rank": 1,
|
|
"node_rank": 0,
|
|
"node_id": "b1e6cbed8533ae2def4e7e7ced9d19858ceb1ed8ab9ba81ab9c07825",
|
|
"node_ip": "10.0.208.100",
|
|
"pid": 76072,
|
|
"gpu_ids": [1],
|
|
"status": "ALIVE",
|
|
"resources": {"CPU": 1}
|
|
}
|
|
],
|
|
"datasets": [
|
|
{
|
|
"name": "train",
|
|
"dataset_name": "train_dataset",
|
|
"dataset_uuid": "1"
|
|
}
|
|
]
|
|
}"""
|
|
|
|
|
|
def _get_run_info_sample(run_id=None, run_name=None) -> TrainRunInfo:
|
|
dataset_info = TrainDatasetInfo(
|
|
name="train", dataset_name="train_dataset", dataset_uuid="1"
|
|
)
|
|
|
|
worker_info_0 = TrainWorkerInfo(
|
|
actor_id="3d86c25634a71832dac32c8802000000",
|
|
world_rank=0,
|
|
local_rank=0,
|
|
node_rank=0,
|
|
node_id="b1e6cbed8533ae2def4e7e7ced9d19858ceb1ed8ab9ba81ab9c07825",
|
|
node_ip="10.0.208.100",
|
|
pid=76071,
|
|
gpu_ids=[0],
|
|
status=ActorStatusEnum.ALIVE,
|
|
resources={"CPU": 1},
|
|
)
|
|
|
|
worker_info_1 = TrainWorkerInfo(
|
|
actor_id="8f162dd8365346d1b5c98ebd7338c4f9",
|
|
world_rank=1,
|
|
local_rank=1,
|
|
node_rank=0,
|
|
node_id="b1e6cbed8533ae2def4e7e7ced9d19858ceb1ed8ab9ba81ab9c07825",
|
|
node_ip="10.0.208.100",
|
|
pid=76072,
|
|
gpu_ids=[1],
|
|
status=ActorStatusEnum.ALIVE,
|
|
resources={"CPU": 1},
|
|
)
|
|
|
|
run_info = TrainRunInfo(
|
|
name=run_name if run_name else "default_run",
|
|
id=run_id if run_id else "ad5256bc64c04c83833a8b006f531799",
|
|
job_id="0000000001",
|
|
controller_actor_id="3abd1972a19148d78acc78dd9414736e",
|
|
workers=[worker_info_0, worker_info_1],
|
|
datasets=[dataset_info],
|
|
start_time_ms=1717448423000,
|
|
run_status=RunStatusEnum.RUNNING,
|
|
status_detail="",
|
|
resources=[{"CPU": 1}, {"CPU": 1}],
|
|
)
|
|
return run_info
|
|
|
|
|
|
def test_schema_equivalance():
|
|
json_sample = RUN_INFO_JSON_SAMPLE
|
|
dict_sample = json.loads(RUN_INFO_JSON_SAMPLE)
|
|
|
|
run_info_from_json = TrainRunInfo.parse_raw(json_sample)
|
|
run_info_from_obj = TrainRunInfo.parse_obj(dict_sample)
|
|
|
|
# Test serialization equivalence
|
|
assert run_info_from_json == run_info_from_obj
|
|
|
|
# Test dict deserialization equivalence
|
|
assert run_info_from_json.dict() == dict_sample
|
|
|
|
# Test json deserialization equivalence
|
|
assert json.loads(run_info_from_json.json()) == json.loads(json_sample)
|
|
|
|
# Test constructors equivalence
|
|
assert _get_run_info_sample() == run_info_from_json
|
|
|
|
|
|
def test_state_actor_api(ray_start_4_cpus):
|
|
state_actor = get_or_create_state_actor()
|
|
named_actors = ray.util.list_named_actors(all_namespaces=True)
|
|
assert {
|
|
"name": TRAIN_STATE_ACTOR_NAME,
|
|
"namespace": TRAIN_STATE_ACTOR_NAMESPACE,
|
|
} in named_actors
|
|
|
|
# Concurrently register 100 runs
|
|
num_runs = 100
|
|
info_list = [_get_run_info_sample(run_id=str(i)) for i in range(num_runs)]
|
|
ray.get([state_actor.register_train_run.remote(run) for run in info_list])
|
|
|
|
# Test get all runs
|
|
train_runs = ray.get(state_actor.get_all_train_runs.remote())
|
|
assert len(train_runs) == num_runs
|
|
|
|
# Test get a single run by run_id
|
|
for i in range(num_runs):
|
|
run_info = ray.get(state_actor.get_train_run.remote(run_id=str(i)))
|
|
assert run_info == info_list[i]
|
|
|
|
|
|
def test_state_manager(ray_start_gpu_cluster):
|
|
worker_group = WorkerGroup(num_workers=4, resources_per_worker={"GPU": 1})
|
|
|
|
# No errors raised if TrainStateActor is not started
|
|
state_manager = TrainRunStateManager(state_actor=None)
|
|
state_manager.register_train_run(
|
|
run_id="run_id",
|
|
run_name="run_name",
|
|
job_id="0000000001",
|
|
controller_actor_id="3abd1972a19148d78acc78dd9414736e",
|
|
datasets={},
|
|
worker_group=worker_group,
|
|
start_time_ms=int(time.time() * 1000),
|
|
run_status=RunStatusEnum.RUNNING,
|
|
resources=[{"CPU": 1}, {"CPU": 1}],
|
|
)
|
|
|
|
# Register 100 runs with 10 TrainRunStateManagers
|
|
state_actor = get_or_create_state_actor()
|
|
for i in range(10):
|
|
state_manager = TrainRunStateManager(state_actor=state_actor)
|
|
for j in range(10):
|
|
run_id = i * 10 + j
|
|
state_manager.register_train_run(
|
|
run_id=str(run_id),
|
|
run_name="run_name",
|
|
job_id="0000000001",
|
|
controller_actor_id="3abd1972a19148d78acc78dd9414736e",
|
|
datasets={
|
|
"train": ray.data.from_items(list(range(4))),
|
|
"eval": ray.data.from_items(list(range(4))),
|
|
},
|
|
worker_group=worker_group,
|
|
start_time_ms=int(time.time() * 1000),
|
|
run_status=RunStatusEnum.RUNNING,
|
|
resources=[{"CPU": 1}, {"CPU": 1}],
|
|
)
|
|
|
|
runs = ray.get(state_actor.get_all_train_runs.remote())
|
|
assert len(runs) == 100
|
|
|
|
for i in range(100):
|
|
run_id = str(i)
|
|
run_info = ray.get(state_actor.get_train_run.remote(run_id=run_id))
|
|
assert run_info and run_info.id == run_id
|
|
|
|
|
|
@pytest.mark.parametrize("gpus_per_worker", [0, 1, 2])
|
|
def test_track_e2e_training(ray_start_gpu_cluster, gpus_per_worker):
|
|
os.environ["RAY_TRAIN_ENABLE_STATE_TRACKING"] = "1"
|
|
num_workers = 4
|
|
run_name = "test"
|
|
datasets = {
|
|
"train": ray.data.from_items(list(range(4))),
|
|
"eval": ray.data.from_items(list(range(4))),
|
|
}
|
|
|
|
if gpus_per_worker == 0:
|
|
use_gpu = False
|
|
resources_per_worker = {"CPU": 1}
|
|
else:
|
|
use_gpu = True
|
|
resources_per_worker = {"GPU": gpus_per_worker}
|
|
|
|
trainer = DataParallelTrainer(
|
|
train_loop_per_worker=lambda: None,
|
|
run_config=RunConfig(name=run_name),
|
|
scaling_config=ScalingConfig(
|
|
num_workers=num_workers,
|
|
use_gpu=use_gpu,
|
|
resources_per_worker=resources_per_worker,
|
|
),
|
|
datasets=datasets,
|
|
)
|
|
|
|
trainer.fit()
|
|
|
|
state_actor = ray.get_actor(
|
|
name=TRAIN_STATE_ACTOR_NAME, namespace=TRAIN_STATE_ACTOR_NAMESPACE
|
|
)
|
|
|
|
runs = ray.get(state_actor.get_all_train_runs.remote())
|
|
run_id = next(iter(runs.keys()))
|
|
run = next(iter(runs.values()))
|
|
|
|
# Check Run Info
|
|
assert run.id == run_id
|
|
assert run.name == run_name
|
|
assert len(run.workers) == num_workers
|
|
assert run.controller_actor_id and run.job_id
|
|
|
|
world_ranks = [worker.world_rank for worker in run.workers]
|
|
local_ranks = [worker.local_rank for worker in run.workers]
|
|
node_ranks = [worker.node_rank for worker in run.workers]
|
|
|
|
# Ensure that the workers are sorted by global rank
|
|
assert world_ranks == [0, 1, 2, 3]
|
|
assert local_ranks == [0, 1, 2, 3]
|
|
assert node_ranks == [0, 0, 0, 0]
|
|
|
|
# Check GPU ids
|
|
gpu_ids = [worker.gpu_ids for worker in run.workers]
|
|
if gpus_per_worker == 0:
|
|
assert gpu_ids == [[], [], [], []]
|
|
elif gpus_per_worker == 1:
|
|
assert gpu_ids == [[0], [1], [2], [3]]
|
|
elif gpus_per_worker == 2:
|
|
flat_gpu_ids = set()
|
|
for ids in gpu_ids:
|
|
flat_gpu_ids.update(ids)
|
|
assert flat_gpu_ids == set(range(8))
|
|
|
|
# Check Datasets
|
|
for dataset_info in run.datasets:
|
|
dataset = datasets[dataset_info.name]
|
|
# DataConfig will automatically set the dataset_name to the key of the dataset dict.
|
|
assert dataset_info.dataset_name == dataset_info.name
|
|
assert dataset_info.dataset_uuid == dataset._uuid
|
|
|
|
|
|
@pytest.mark.parametrize("raise_error", [True, False])
|
|
def test_train_run_status(ray_start_gpu_cluster, raise_error):
|
|
os.environ["RAY_TRAIN_ENABLE_STATE_TRACKING"] = "1"
|
|
|
|
def get_train_run():
|
|
state_actor = ray.get_actor(
|
|
name=TRAIN_STATE_ACTOR_NAME, namespace=TRAIN_STATE_ACTOR_NAMESPACE
|
|
)
|
|
runs = ray.get(state_actor.get_all_train_runs.remote())
|
|
return next(iter(runs.values()))
|
|
|
|
def check_run_status(expected_status):
|
|
run = get_train_run()
|
|
assert run.run_status == expected_status
|
|
|
|
def check_run_error(failed_rank, error_message):
|
|
run = get_train_run()
|
|
assert run.status_detail
|
|
assert f"Rank {failed_rank} worker raised an error" in run.status_detail
|
|
assert error_message in run.status_detail
|
|
|
|
failed_rank = 0
|
|
error_message = "User Application Error"
|
|
|
|
def train_func():
|
|
check_run_status(expected_status=RunStatusEnum.RUNNING)
|
|
if raise_error and ray.train.get_context().get_world_rank() == failed_rank:
|
|
raise RuntimeError(error_message)
|
|
|
|
trainer = DataParallelTrainer(
|
|
train_loop_per_worker=train_func,
|
|
scaling_config=ScalingConfig(num_workers=4, use_gpu=False),
|
|
)
|
|
|
|
try:
|
|
trainer.fit()
|
|
except Exception:
|
|
pass
|
|
|
|
if raise_error:
|
|
check_run_status(expected_status=RunStatusEnum.ERRORED)
|
|
check_run_error(failed_rank=failed_rank, error_message=error_message)
|
|
else:
|
|
check_run_status(expected_status=RunStatusEnum.FINISHED)
|
|
|
|
ray.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|