Files
ray-project--ray/python/ray/tune/tests/test_train_v2_integration.py
T
2026-07-13 13:17:40 +08:00

140 lines
4.3 KiB
Python

import sys
import pytest
import ray.train
import ray.tune
from ray.cluster_utils import Cluster
from ray.train.tests.util import create_dict_checkpoint
from ray.train.v2._internal.constants import HEALTH_CHECK_INTERVAL_S_ENV_VAR
from ray.train.v2.api.data_parallel_trainer import DataParallelTrainer
from ray.tune.integration.ray_train import CHECKPOINT_PATH_KEY, TuneReportCallback
TRAIN_DRIVER_RESOURCE_NAME = "train_driver_resource"
NUM_GPUS_IN_CLUSTER = 4
@pytest.fixture()
def ray_start_4_cpus():
ray.init(num_cpus=4)
yield
ray.shutdown()
@pytest.fixture()
def ray_cpu_head_gpu_worker():
cluster = Cluster()
cluster.add_node(resources={TRAIN_DRIVER_RESOURCE_NAME: 1})
cluster.add_node(num_cpus=0, num_gpus=NUM_GPUS_IN_CLUSTER)
ray.init(address=cluster.address)
yield
ray.shutdown()
cluster.shutdown()
@pytest.fixture(autouse=True)
def speed_up_tests(monkeypatch):
monkeypatch.setenv(HEALTH_CHECK_INTERVAL_S_ENV_VAR, "0.1")
@pytest.mark.parametrize("num_workers_grid_search", [[1], [1, 2, 4]])
@pytest.mark.parametrize("limit_concurrency", [True, False])
def test_e2e(
ray_cpu_head_gpu_worker,
tmp_path,
num_workers_grid_search,
limit_concurrency,
):
num_non_checkpoint_reports = 2
num_checkpoint_reports = 1
def train_fn_per_worker(train_fn_config):
assert "lr" in train_fn_config
world_size = ray.train.get_context().get_world_size()
for i in range(num_non_checkpoint_reports):
ray.train.report({"idx": i})
for i in range(num_checkpoint_reports):
with create_dict_checkpoint({"model": "dummy"}) as checkpoint:
ray.train.report(
{"loss": 0.1, "world_size": world_size}, checkpoint=checkpoint
)
def launch_training(tune_config):
trainer = DataParallelTrainer(
train_loop_per_worker=train_fn_per_worker,
train_loop_config=tune_config["train_loop_config"],
scaling_config=ray.train.ScalingConfig(
num_workers=tune_config["num_workers"], use_gpu=True
),
run_config=ray.train.RunConfig(
storage_path=tmp_path,
name=f"train-{ray.tune.get_context().get_trial_id()}",
callbacks=[TuneReportCallback()],
),
)
trainer.fit()
tuner = ray.tune.Tuner(
ray.tune.with_resources(launch_training, {TRAIN_DRIVER_RESOURCE_NAME: 0.01}),
param_space={
# Search over parameters passed into each train worker.
"train_loop_config": {"lr": ray.tune.choice([0.01, 0.001])},
# Search over Train "run level" parameters.
"num_workers": ray.tune.grid_search(num_workers_grid_search),
},
tune_config=ray.tune.TuneConfig(
max_concurrent_trials=(
NUM_GPUS_IN_CLUSTER // max(num_workers_grid_search)
if limit_concurrency
else None
)
),
run_config=ray.tune.RunConfig(storage_path=tmp_path, name="tune"),
)
result_grid = tuner.fit()
assert len(result_grid) == len(num_workers_grid_search)
world_sizes = set()
for result in result_grid:
assert (
len(result.metrics_dataframe)
== num_non_checkpoint_reports + num_checkpoint_reports
)
assert "loss" in result.metrics
assert CHECKPOINT_PATH_KEY in result.metrics
world_sizes.add(result.metrics["world_size"])
assert world_sizes == set(num_workers_grid_search)
def test_errors(ray_start_4_cpus):
"""Test that errors in training are properly captured and reported."""
def train_worker_fn():
raise RuntimeError("Simulated training error")
def train_fn(config):
trainer = DataParallelTrainer(train_worker_fn)
trainer.fit()
tuner = ray.tune.Tuner(train_fn)
results = tuner.fit()
assert results.errors, "Expected errors to be captured"
assert len(results.errors) == 1, "Expected exactly one error"
error = results.errors[0]
assert "RuntimeError" in str(error), f"Expected RuntimeError, got: {error}"
assert "Simulated training error" in str(
error
), f"Expected specific error message, got: {error}"
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-x", __file__]))