396 lines
11 KiB
Python
396 lines
11 KiB
Python
import functools
|
|
import sys
|
|
import time
|
|
from contextlib import nullcontext
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray import train
|
|
from ray.air._internal.util import StartTraceback
|
|
from ray.train import DataConfig
|
|
from ray.train._internal.backend_executor import BackendExecutor
|
|
from ray.train._internal.session import get_session, init_session
|
|
from ray.train._internal.utils import construct_train_func
|
|
from ray.train._internal.worker_group import WorkerGroup
|
|
from ray.train.backend import BackendConfig
|
|
from ray.train.examples.pytorch.torch_linear_example import (
|
|
train_func as linear_train_func,
|
|
)
|
|
from ray.train.tests.util import mock_storage_context
|
|
from ray.train.trainer import TrainingIterator
|
|
|
|
MAX_RETRIES = 3
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="module")
|
|
def patch_tune_session():
|
|
if not get_session():
|
|
init_session(
|
|
training_func=None,
|
|
world_rank=None,
|
|
local_rank=None,
|
|
node_rank=None,
|
|
local_world_size=None,
|
|
world_size=None,
|
|
storage=mock_storage_context(),
|
|
)
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_4_cpus():
|
|
address_info = ray.init(num_cpus=4)
|
|
yield address_info
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
def gen_execute_single_async_special(special_f):
|
|
def execute_single_async_special(self, i, f, *args, **kwargs):
|
|
assert len(self.workers) == 2
|
|
if i == 0 and hasattr(self, "should_fail") and self.should_fail:
|
|
kwargs["train_func"] = special_f
|
|
return (
|
|
self.workers[i]
|
|
.actor._RayTrainWorker__execute.options(name=f.__name__)
|
|
.remote(f, *args, **kwargs)
|
|
)
|
|
|
|
return execute_single_async_special
|
|
|
|
|
|
def gen_new_backend_executor(special_f):
|
|
"""Returns a BackendExecutor that runs special_f on worker 0 once."""
|
|
|
|
class TestBackendExecutor(BackendExecutor):
|
|
_has_failed = False
|
|
|
|
def start_training(self, *args, **kwargs):
|
|
special_execute = gen_execute_single_async_special(special_f)
|
|
if not self._has_failed:
|
|
self.worker_group.should_fail = True
|
|
self._has_failed = True
|
|
else:
|
|
self.worker_group.should_fail = False
|
|
with patch.object(WorkerGroup, "execute_single_async", special_execute):
|
|
super().start_training(*args, **kwargs)
|
|
|
|
return TestBackendExecutor
|
|
|
|
|
|
def create_iterator(
|
|
train_func,
|
|
backend_config,
|
|
*,
|
|
num_workers=2,
|
|
backend_executor_cls=BackendExecutor,
|
|
init_hook=None,
|
|
):
|
|
# Similar logic to the old Trainer.run_iterator().
|
|
|
|
train_func = construct_train_func(train_func, None, train_func_context=nullcontext)
|
|
|
|
backend_executor = backend_executor_cls(
|
|
backend_config=backend_config, num_workers=num_workers, max_retries=MAX_RETRIES
|
|
)
|
|
backend_executor.start(init_hook)
|
|
|
|
return TrainingIterator(
|
|
backend_executor=backend_executor,
|
|
backend_config=backend_config,
|
|
train_func=train_func,
|
|
datasets={},
|
|
metadata={},
|
|
data_config=DataConfig(),
|
|
checkpoint=None,
|
|
)
|
|
|
|
|
|
def test_run_iterator(ray_start_4_cpus):
|
|
config = BackendConfig()
|
|
|
|
def train_func():
|
|
for i in range(3):
|
|
train.report(dict(index=i))
|
|
return 1
|
|
|
|
iterator = create_iterator(train_func, config)
|
|
|
|
count = 0
|
|
for results in iterator:
|
|
assert all(value.metrics["index"] == count for value in results)
|
|
count += 1
|
|
|
|
assert count == 3
|
|
assert iterator.is_finished()
|
|
|
|
with pytest.raises(StopIteration):
|
|
next(iterator)
|
|
|
|
|
|
def test_run_iterator_error(ray_start_4_cpus):
|
|
config = BackendConfig()
|
|
|
|
def fail_train():
|
|
raise NotImplementedError
|
|
|
|
iterator = create_iterator(fail_train, config)
|
|
|
|
with pytest.raises(StartTraceback) as exc:
|
|
next(iterator)
|
|
|
|
assert isinstance(exc.value.__cause__, NotImplementedError), (
|
|
exc.value,
|
|
exc.value.__cause__,
|
|
)
|
|
|
|
assert iterator.is_finished()
|
|
|
|
|
|
def test_worker_failure_1(ray_start_4_cpus):
|
|
def train_func():
|
|
train.report({"test": 1})
|
|
|
|
def train_actor_failure():
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
|
|
new_backend_executor_cls = gen_new_backend_executor(train_actor_failure)
|
|
|
|
config = BackendConfig()
|
|
|
|
iterator = create_iterator(
|
|
train_func, config, backend_executor_cls=new_backend_executor_cls
|
|
)
|
|
for worker_results in iterator:
|
|
assert all(result.metrics["test"] == 1 for result in worker_results)
|
|
|
|
|
|
def test_worker_failure_2(ray_start_4_cpus):
|
|
def train_func():
|
|
for _ in range(2):
|
|
train.report(dict(loss=1))
|
|
|
|
def train_actor_failure():
|
|
for _ in range(2):
|
|
train.report(dict(loss=1))
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
|
|
new_backend_executor_cls = gen_new_backend_executor(train_actor_failure)
|
|
|
|
config = BackendConfig()
|
|
|
|
iterator = create_iterator(
|
|
train_func, config, backend_executor_cls=new_backend_executor_cls
|
|
)
|
|
for worker_results in iterator:
|
|
assert all(result.metrics["loss"] == 1 for result in worker_results)
|
|
|
|
|
|
def test_worker_failure_local_rank(ray_start_4_cpus):
|
|
def train_func():
|
|
train.report({"rank": train.get_context().get_local_rank()})
|
|
|
|
def train_actor_failure():
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
|
|
new_backend_executor_cls = gen_new_backend_executor(train_actor_failure)
|
|
|
|
config = BackendConfig()
|
|
|
|
iterator = create_iterator(
|
|
train_func, config, backend_executor_cls=new_backend_executor_cls
|
|
)
|
|
for worker_results in iterator:
|
|
assert {result.metrics["rank"] for result in worker_results} == {0, 1}
|
|
|
|
|
|
def test_worker_start_failure(ray_start_4_cpus):
|
|
def init_hook():
|
|
pass
|
|
|
|
def init_hook_fail():
|
|
ray.actor.exit_actor()
|
|
|
|
class TestBackendExecutor(BackendExecutor):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def _restart(self):
|
|
self._initialization_hook = init_hook
|
|
super()._restart()
|
|
|
|
config = BackendConfig()
|
|
|
|
iterator = create_iterator(
|
|
lambda x: 1,
|
|
config,
|
|
backend_executor_cls=TestBackendExecutor,
|
|
init_hook=init_hook_fail,
|
|
)
|
|
assert len(iterator._backend_executor.get_worker_group()) == 2
|
|
|
|
|
|
def test_max_failures(ray_start_4_cpus):
|
|
def train_func():
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
|
|
config = BackendConfig()
|
|
|
|
iterator = create_iterator(train_func, config)
|
|
with pytest.raises(RuntimeError):
|
|
for _ in iterator:
|
|
pass
|
|
assert iterator._backend_executor._get_num_failures() == MAX_RETRIES
|
|
|
|
|
|
def test_start_max_failures(ray_start_4_cpus):
|
|
def init_hook_fail():
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
|
|
config = BackendConfig()
|
|
|
|
with pytest.raises(RuntimeError):
|
|
create_iterator(lambda x: 1, config, init_hook=init_hook_fail)
|
|
|
|
|
|
class KillCallback:
|
|
def __init__(self, fail_on, backend_executor):
|
|
self.counter = 0
|
|
self.fail_on = fail_on
|
|
self.worker_group = backend_executor.get_worker_group()
|
|
self.results = []
|
|
|
|
def handle_result(self, intermiedate_results=None):
|
|
if intermiedate_results:
|
|
self.results.append(intermiedate_results)
|
|
if self.counter == self.fail_on:
|
|
print("killing")
|
|
self.results = []
|
|
ray.kill(self.worker_group.workers[0].actor)
|
|
time.sleep(3)
|
|
self.counter += 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"backend",
|
|
["test", "torch", "tf"] if sys.version_info < (3, 12) else ["test", "torch"],
|
|
)
|
|
def test_worker_kill(ray_start_4_cpus, backend):
|
|
if backend == "test":
|
|
test_config = BackendConfig()
|
|
elif backend == "torch":
|
|
from ray.train.torch import TorchConfig
|
|
|
|
test_config = TorchConfig()
|
|
elif backend == "tf":
|
|
from ray.train.tensorflow import TensorflowConfig
|
|
|
|
test_config = TensorflowConfig()
|
|
|
|
def train_func():
|
|
for i in range(2):
|
|
train.report(dict(loss=1, iter=i))
|
|
|
|
iterator = create_iterator(train_func, test_config)
|
|
kill_callback = KillCallback(fail_on=0, backend_executor=iterator._backend_executor)
|
|
|
|
for intermediate_result in iterator:
|
|
# Run 1: iter=0, counter=1, Successful
|
|
# Run 2: iter=1, counter=1, Unsuccessful, starts training from beginning
|
|
# Run 3: iter=0, counter=2, Successful
|
|
# Run 4: iter=1, counter=3, Successful
|
|
kill_callback.handle_result()
|
|
assert kill_callback.counter == 3
|
|
|
|
iterator = create_iterator(train_func, test_config)
|
|
kill_callback = KillCallback(fail_on=1, backend_executor=iterator._backend_executor)
|
|
for intermediate_result in iterator:
|
|
# Run 1: iter=0, counter=1, Successful
|
|
# Run 2: iter=1, counter=2, Successful
|
|
# Run 3: None, counter=2, Unsuccessful, starts training from beginning.
|
|
# Run 4: iter=0, counter=3, Successful
|
|
# Run 5: iter=1, counter=4, Successful
|
|
kill_callback.handle_result()
|
|
assert kill_callback.counter == 4
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.version_info >= (3, 12), reason="tensorflow is not installed in python 3.12+"
|
|
)
|
|
def test_tensorflow_mnist_fail(ray_start_4_cpus):
|
|
"""Tests if tensorflow example works even with worker failure."""
|
|
epochs = 3
|
|
num_workers = 2
|
|
|
|
from ray.train.examples.tf.tensorflow_mnist_example import (
|
|
train_func as tensorflow_mnist_train_func,
|
|
)
|
|
from ray.train.tensorflow import TensorflowConfig
|
|
|
|
test_config = TensorflowConfig()
|
|
|
|
train_func = functools.partial(
|
|
tensorflow_mnist_train_func, {"lr": 1e-3, "batch_size": 64, "epochs": epochs}
|
|
)
|
|
iterator = create_iterator(train_func, test_config, num_workers=num_workers)
|
|
kill_callback = KillCallback(fail_on=0, backend_executor=iterator._backend_executor)
|
|
|
|
for intermediate_result in iterator:
|
|
assert len(intermediate_result) == num_workers
|
|
kill_callback.handle_result(intermediate_result)
|
|
|
|
results = kill_callback.results
|
|
assert len(results) == epochs
|
|
last_iter_result = results[-1][0].metrics
|
|
first_iter_result = results[0][0].metrics
|
|
|
|
assert last_iter_result["loss"] < first_iter_result["loss"]
|
|
assert last_iter_result["accuracy"] > first_iter_result["accuracy"]
|
|
|
|
|
|
def test_torch_linear_failure(ray_start_4_cpus):
|
|
num_workers = 2
|
|
epochs = 3
|
|
|
|
from ray.train.torch import TorchConfig
|
|
|
|
test_config = TorchConfig()
|
|
|
|
train_func = functools.partial(
|
|
linear_train_func, {"lr": 1e-3, "batch_size": 64, "epochs": epochs}
|
|
)
|
|
|
|
iterator = create_iterator(train_func, test_config, num_workers=num_workers)
|
|
kill_callback = KillCallback(fail_on=1, backend_executor=iterator._backend_executor)
|
|
|
|
for intermediate_result in iterator:
|
|
assert len(intermediate_result) == num_workers
|
|
kill_callback.handle_result(intermediate_result)
|
|
|
|
results = kill_callback.results
|
|
assert len(results) == epochs
|
|
for i in range(num_workers):
|
|
last_result = results[-1][i].metrics
|
|
first_result = results[0][i].metrics
|
|
assert last_result["loss"] < first_result["loss"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.exit(pytest.main(sys.argv[1:] + ["-v", "-x", __file__]))
|