chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Trigger pytest hook to automatically zip test cluster logs to archive dir on failure
|
||||
from ray.tests.conftest import propagate_logs # noqa
|
||||
from ray.tests.conftest import pytest_runtest_makereport # noqa
|
||||
@@ -0,0 +1,131 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.tune import PlacementGroupFactory
|
||||
from ray.tune.tests.execution.utils import TestingTrial, create_execution_test_objects
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_2_cpus():
|
||||
address_info = ray.init(num_cpus=2)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
def test_actor_cached(tmpdir, ray_start_2_cpus):
|
||||
tune_controller, actor_manger, resource_manager = create_execution_test_objects(
|
||||
max_pending_trials=8
|
||||
)
|
||||
|
||||
assert not actor_manger.added_actors
|
||||
|
||||
tune_controller.add_trial(TestingTrial("trainable1", stub=True, trial_id="trial1"))
|
||||
tune_controller.step()
|
||||
|
||||
tracked_actor, cls_name, kwargs = actor_manger.added_actors[0]
|
||||
assert cls_name == "trainable1"
|
||||
|
||||
|
||||
def test_actor_reuse_unstaged(tmpdir, ray_start_2_cpus):
|
||||
"""A trial that hasn't been staged can re-use an actor.
|
||||
|
||||
In specific circumstances, this can lead to errors. Notably, when an
|
||||
external source (e.g. a scheduler) directly calls TuneController APIs,
|
||||
we can be in a situation where a trial has not been staged, but there is
|
||||
still an actor available for it to use (because it hasn't been evicted from
|
||||
the cache, yet).
|
||||
|
||||
This test constructs such a situation an asserts that actor re-use does not
|
||||
lead to errors in those cases.
|
||||
"""
|
||||
tune_controller, actor_manger, resource_manager = create_execution_test_objects(
|
||||
max_pending_trials=1
|
||||
)
|
||||
tune_controller._reuse_actors = True
|
||||
|
||||
assert not actor_manger.added_actors
|
||||
|
||||
trialA1 = TestingTrial(
|
||||
"trainable1",
|
||||
stub=True,
|
||||
trial_id="trialA1",
|
||||
placement_group_factory=PlacementGroupFactory([{"CPU": 1}]),
|
||||
)
|
||||
tune_controller.add_trial(trialA1)
|
||||
trialB1 = TestingTrial(
|
||||
"trainable1",
|
||||
stub=True,
|
||||
trial_id="trialB1",
|
||||
placement_group_factory=PlacementGroupFactory([{"CPU": 5}]),
|
||||
)
|
||||
tune_controller.add_trial(trialB1)
|
||||
trialA2 = TestingTrial(
|
||||
"trainable1",
|
||||
stub=True,
|
||||
trial_id="trialA2",
|
||||
placement_group_factory=PlacementGroupFactory([{"CPU": 1}]),
|
||||
)
|
||||
tune_controller.add_trial(trialA2)
|
||||
tune_controller.step()
|
||||
|
||||
# Prevent trial A3 from being staged by setting the number
|
||||
# of pending actors to the maximum allowed
|
||||
actor_manger.set_num_pending(2)
|
||||
|
||||
trialA3 = TestingTrial(
|
||||
"trainable1",
|
||||
stub=True,
|
||||
trial_id="trialA3",
|
||||
placement_group_factory=PlacementGroupFactory([{"CPU": 1}]),
|
||||
)
|
||||
tune_controller.add_trial(trialA3)
|
||||
tune_controller.step()
|
||||
|
||||
tracked_actorA1, _, _ = actor_manger.added_actors[0]
|
||||
tracked_actorB1, _, _ = actor_manger.added_actors[1]
|
||||
tracked_actorA2, _, _ = actor_manger.added_actors[2]
|
||||
|
||||
# Start trial A1, report that it's done training.
|
||||
# This will cache the actor for A1 as A2 is already scheduled.
|
||||
tune_controller._actor_started(tracked_actorA1)
|
||||
tune_controller._on_training_result(trialA1, {"done": True})
|
||||
|
||||
# Trial A2 should be in the staged trials. A3 should still not be staged.
|
||||
assert trialA2 in tune_controller._staged_trials
|
||||
assert trialA3 not in tune_controller._staged_trials
|
||||
|
||||
# The actor of A1 should be cached for re-use now.
|
||||
assert tune_controller._actor_cache.num_cached_objects == 1
|
||||
|
||||
# In the meantime, actor A2 started. This will unstage it.
|
||||
tune_controller._actor_started(tracked_actorA2)
|
||||
|
||||
# Now, an external source (e.g. the BOHB scheduler) wants to prematurely
|
||||
# stop trial A2. This will leave the cached actor intact, but trial A3
|
||||
# is still not scheduled.
|
||||
tune_controller._schedule_trial_stop(trialA2)
|
||||
assert tune_controller._actor_cache.num_cached_objects == 1
|
||||
|
||||
# Process events. This will invoke "path 3" in TuneController._maybe_add_actors
|
||||
# and re-use the cached actor
|
||||
tune_controller.step()
|
||||
|
||||
# Reset future scheduled
|
||||
assert actor_manger.scheduled_futures[-1][2] == "reset"
|
||||
|
||||
# Prior to https://github.com/ray-project/ray/pull/36951, there was a bug here:
|
||||
# Because trial A3 was never staged, the unstage ran into an error.
|
||||
# This fails without the line: self._staged_trials.add(start_trial)
|
||||
tune_controller._on_trial_reset(trialA3, True)
|
||||
|
||||
# When the actor finally stops, the cache size is adjusted and the actor is
|
||||
# evicted. This test failed without the line:
|
||||
# self._actor_cache.increase_max(start_trial.placement_group_factory)
|
||||
tune_controller._actor_stopped(tracked_actorA1)
|
||||
tune_controller.step()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,69 @@
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import Callback, ResumeConfig
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
|
||||
|
||||
class StatefulCallback(Callback):
|
||||
CKPT_FILE_TMPL = "test-callback-state-{}.json"
|
||||
|
||||
def __init__(self):
|
||||
self.counter = 0
|
||||
|
||||
def on_trial_result(self, iteration, trials, trial, result, **info):
|
||||
self.counter += 1
|
||||
|
||||
def get_state(self) -> Optional[Dict]:
|
||||
return {"counter": self.counter}
|
||||
|
||||
def set_state(self, state: Dict):
|
||||
self.counter = state["counter"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_callback_save_restore(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmpdir
|
||||
):
|
||||
"""Check that callback state is restored correctly.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testCallbackSaveRestore
|
||||
"""
|
||||
storage = mock_storage_context()
|
||||
runner = TuneController(callbacks=[StatefulCallback()], storage=storage)
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, stub=True, storage=storage))
|
||||
for i in range(3):
|
||||
runner._callbacks.on_trial_result(
|
||||
iteration=i, trials=None, trial=None, result=None
|
||||
)
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
callback = StatefulCallback()
|
||||
runner2 = TuneController(callbacks=[callback], storage=storage)
|
||||
assert callback.counter == 0
|
||||
runner2.resume(resume_config=ResumeConfig())
|
||||
assert callback.counter == 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,619 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.constants import TRAINING_ITERATION
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train._internal.session import _TrainingResult
|
||||
from ray.train._internal.storage import StorageContext
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import (
|
||||
Callback,
|
||||
Checkpoint,
|
||||
CheckpointConfig,
|
||||
PlacementGroupFactory,
|
||||
ResumeConfig,
|
||||
)
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.result import DONE
|
||||
from ray.tune.schedulers import FIFOScheduler
|
||||
from ray.tune.search import BasicVariantGenerator
|
||||
from ray.tune.tests.tune_test_util import TrialResultObserver
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
STORAGE = mock_storage_context()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
def create_mock_components():
|
||||
class _MockScheduler(FIFOScheduler):
|
||||
errored_trials = []
|
||||
|
||||
def on_trial_error(self, tune_controller, trial):
|
||||
self.errored_trials += [trial]
|
||||
|
||||
class _MockSearchAlg(BasicVariantGenerator):
|
||||
errored_trials = []
|
||||
|
||||
def on_trial_complete(self, trial_id, error=False, **kwargs):
|
||||
if error:
|
||||
self.errored_trials += [trial_id]
|
||||
|
||||
searchalg = _MockSearchAlg()
|
||||
scheduler = _MockScheduler()
|
||||
return searchalg, scheduler
|
||||
|
||||
|
||||
def num_checkpoints(trial):
|
||||
return sum(
|
||||
item.startswith("checkpoint_")
|
||||
for item in os.listdir(trial.storage.trial_fs_path)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_save_restore(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmpdir
|
||||
):
|
||||
"""Test that a checkpoint is saved and can be used to restore a trainable.
|
||||
|
||||
The trainable saves a checkpoint and terminates. We then start another trial
|
||||
that should restore from the saved checkpoint and assert that it picks up
|
||||
the state and continues to run to termination.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testCheckpointing
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testRestoreMetricsAfterCheckpointing # noqa
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(), storage=STORAGE
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 1},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"checkpoint_config": CheckpointConfig(checkpoint_frequency=1),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
trials = runner.get_trials()
|
||||
|
||||
runner.step() # Start trial
|
||||
|
||||
while trials[0].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
while trials[0].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].latest_checkpoint_result.metrics[TRAINING_ITERATION] == 1
|
||||
assert trials[0].last_result[TRAINING_ITERATION] == 1
|
||||
assert trials[0].last_result["iterations_since_restore"] == 1
|
||||
|
||||
# Prepare new trial
|
||||
kwargs["restore_path"] = trials[0].checkpoint.path
|
||||
new_trial = Trial(MOCK_TRAINABLE_NAME, **kwargs)
|
||||
runner.add_trial(new_trial)
|
||||
trials = runner.get_trials()
|
||||
|
||||
assert trials[1].status == Trial.PENDING
|
||||
|
||||
# Start trial, restore, run to termination
|
||||
while trials[1].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
# Restore
|
||||
runner.step()
|
||||
|
||||
# Run to termination
|
||||
while trials[1].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].latest_checkpoint_result.metrics[TRAINING_ITERATION] == 1
|
||||
assert trials[1].last_result[TRAINING_ITERATION] == 1
|
||||
assert trials[1].last_result["iterations_since_restore"] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_at_end(ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmpdir):
|
||||
"""Test that a checkpoint is saved at end for class trainables with that config.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testCheckpointingAtEnd
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testResultDone
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 2},
|
||||
"checkpoint_config": CheckpointConfig(checkpoint_at_end=True),
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
trials = runner.get_trials()
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert trials[0].has_checkpoint()
|
||||
assert trials[0].last_result[DONE]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_pause_resume_trial(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmpdir
|
||||
):
|
||||
"""Test that trial that is paused and resumed picks up its last checkpoint.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testPauseThenResume
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 2},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"checkpoint_config": CheckpointConfig(checkpoint_frequency=1),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
trials = runner.get_trials()
|
||||
|
||||
while trials[0].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
runner._schedule_trial_pause(trials[0], should_checkpoint=True)
|
||||
|
||||
while trials[0].status != Trial.PAUSED:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].has_checkpoint()
|
||||
assert not trials[0].last_result.get(DONE), trials[0].last_result
|
||||
|
||||
# Start again
|
||||
runner._set_trial_status(trials[0], Trial.PENDING)
|
||||
|
||||
while trials[0].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
while trials[0].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].checkpoint
|
||||
assert trials[0].last_result[TRAINING_ITERATION] == 2
|
||||
assert trials[0].last_result["iterations_since_restore"] == 1
|
||||
assert trials[0].last_result["time_since_restore"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_num_to_keep(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmp_path
|
||||
):
|
||||
"""Test that only num_to_keep checkpoints are kept.
|
||||
|
||||
This should also hold true when the experiment is resumed.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testPauseResumeCheckpointCount
|
||||
"""
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=CheckpointConfig(num_to_keep=2),
|
||||
storage=STORAGE,
|
||||
)
|
||||
trial.init_local_path()
|
||||
|
||||
def write_checkpoint(trial: Trial, index: int):
|
||||
checkpoint_dir = tmp_path / StorageContext._make_checkpoint_dir_name(index)
|
||||
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = {"training_iteration": index}
|
||||
with open(os.path.join(checkpoint_dir, "cp.json"), "w") as f:
|
||||
json.dump(result, f)
|
||||
|
||||
checkpoint = Checkpoint.from_directory(checkpoint_dir)
|
||||
return _TrainingResult(checkpoint=checkpoint, metrics=result)
|
||||
|
||||
def get_checkpoint_dirs(trial: Trial):
|
||||
return [d for d in os.listdir(tmp_path) if d.startswith("checkpoint_")]
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(), storage=STORAGE
|
||||
)
|
||||
|
||||
runner.add_trial(trial)
|
||||
|
||||
# Write 1 checkpoint
|
||||
result = write_checkpoint(trial, 1)
|
||||
runner._on_saving_result(trial, result)
|
||||
|
||||
# Expect 1 checkpoint
|
||||
cp_dirs = get_checkpoint_dirs(trial)
|
||||
assert len(cp_dirs) == 1, f"Checkpoint dirs: {cp_dirs}"
|
||||
|
||||
# Write second checkpoint
|
||||
result = write_checkpoint(trial, 2)
|
||||
runner._on_saving_result(trial, result)
|
||||
|
||||
# Expect 2 checkpoints
|
||||
cp_dirs = get_checkpoint_dirs(trial)
|
||||
assert len(cp_dirs) == 2, f"Checkpoint dirs: {cp_dirs}"
|
||||
|
||||
# Write third checkpoint
|
||||
result = write_checkpoint(trial, 3)
|
||||
runner._on_saving_result(trial, result)
|
||||
|
||||
# Expect 2 checkpoints because num_to_keep = 2
|
||||
cp_dirs = get_checkpoint_dirs(trial)
|
||||
assert len(cp_dirs) == 2, f"Checkpoint dirs: {cp_dirs}"
|
||||
|
||||
# Re-instantiate trial runner and resume
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
resume_config=ResumeConfig(),
|
||||
)
|
||||
|
||||
trial = runner.get_trials()[0]
|
||||
|
||||
# Write fourth checkpoint
|
||||
result = write_checkpoint(trial, 4)
|
||||
runner._on_saving_result(trial, result)
|
||||
|
||||
# Expect 2 checkpoints because num_to_keep = 2
|
||||
cp_dirs = get_checkpoint_dirs(trial)
|
||||
assert len(cp_dirs) == 2, f"Checkpoint dirs: {cp_dirs}"
|
||||
|
||||
# Write fifth checkpoint
|
||||
result = write_checkpoint(trial, 5)
|
||||
runner._on_saving_result(trial, result)
|
||||
|
||||
# Expect 2 checkpoints because num_to_keep = 2
|
||||
cp_dirs = get_checkpoint_dirs(trial)
|
||||
assert len(cp_dirs) == 2, f"Checkpoint dirs: {cp_dirs}"
|
||||
|
||||
# Checkpoints before restore should be deleted
|
||||
assert "checkpoint_000004" in cp_dirs
|
||||
assert "checkpoint_000005" in cp_dirs
|
||||
|
||||
assert "checkpoint_000002" not in cp_dirs
|
||||
assert "checkpoint_000003" not in cp_dirs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_freq_buffered(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmp_path
|
||||
):
|
||||
"""Test that trial checkpoints are a lower bound for buffered training iterations.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testCheckpointFreqBuffered
|
||||
"""
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"TUNE_RESULT_BUFFER_LENGTH": "7", "TUNE_RESULT_BUFFER_MIN_TIME_S": "1"},
|
||||
):
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=CheckpointConfig(checkpoint_frequency=3),
|
||||
storage=STORAGE,
|
||||
)
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
checkpoint_period=0,
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
|
||||
while not trial.is_saving:
|
||||
runner.step()
|
||||
runner.step()
|
||||
assert trial.last_result[TRAINING_ITERATION] == 3
|
||||
assert num_checkpoints(trial) == 1
|
||||
|
||||
while not trial.is_saving:
|
||||
runner.step()
|
||||
runner.step()
|
||||
assert trial.last_result[TRAINING_ITERATION] == 6
|
||||
assert num_checkpoints(trial) == 2
|
||||
|
||||
while not trial.is_saving:
|
||||
runner.step()
|
||||
runner.step()
|
||||
assert trial.last_result[TRAINING_ITERATION] == 9
|
||||
assert num_checkpoints(trial) == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_at_end_not_buffered(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmp_path
|
||||
):
|
||||
"""Test that trials with `checkpoint_at_end=True` are never buffered.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testCheckpointAtEndNotBuffered
|
||||
"""
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"TUNE_RESULT_BUFFER_LENGTH": "7", "TUNE_RESULT_BUFFER_MIN_TIME_S": "0.5"},
|
||||
):
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=CheckpointConfig(
|
||||
checkpoint_at_end=True,
|
||||
),
|
||||
stopping_criterion={"training_iteration": 4},
|
||||
storage=STORAGE,
|
||||
)
|
||||
observer = TrialResultObserver()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
callbacks=[observer],
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
|
||||
while not observer.just_received_a_result():
|
||||
runner.step()
|
||||
assert trial.last_result[TRAINING_ITERATION] == 1
|
||||
assert num_checkpoints(trial) == 0
|
||||
|
||||
while True:
|
||||
runner.step()
|
||||
if observer.just_received_a_result():
|
||||
break
|
||||
assert trial.last_result[TRAINING_ITERATION] == 2
|
||||
assert num_checkpoints(trial) == 0
|
||||
|
||||
while True:
|
||||
runner.step()
|
||||
if observer.just_received_a_result():
|
||||
break
|
||||
assert trial.last_result[TRAINING_ITERATION] == 3
|
||||
assert num_checkpoints(trial) == 0
|
||||
|
||||
while True:
|
||||
runner.step()
|
||||
if observer.just_received_a_result():
|
||||
break
|
||||
assert trial.last_result[TRAINING_ITERATION] == 4
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
assert num_checkpoints(trial) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_checkpoint_auto_period(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmp_path
|
||||
):
|
||||
"""Test that the checkpoint auto period is adjusted when syncing takes a long time.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testCheckpointAutoPeriod
|
||||
"""
|
||||
storage = mock_storage_context()
|
||||
|
||||
with tempfile.TemporaryDirectory() as local_dir:
|
||||
storage.storage_local_path = local_dir
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=storage,
|
||||
checkpoint_period="auto",
|
||||
)
|
||||
|
||||
with mock.patch.object(runner, "save_to_dir") as save_to_dir:
|
||||
save_to_dir.side_effect = lambda *a, **kw: time.sleep(2)
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, storage=storage))
|
||||
runner.step() # Run one step, this will trigger checkpointing
|
||||
|
||||
assert runner._checkpoint_manager._checkpoint_period > 38.0
|
||||
|
||||
|
||||
def test_checkpoint_force_with_num_to_keep(ray_start_4_cpus_2_gpus_extra, tmp_path):
|
||||
"""Test that cloud syncing is forced if one of the trials has made more
|
||||
than num_to_keep checkpoints since last sync.
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::
|
||||
testCloudCheckpointForceWithNumToKeep
|
||||
"""
|
||||
storage = mock_storage_context()
|
||||
# Needed to avoid infinite recursion error on CI runners
|
||||
storage.syncer.__getstate__ = lambda *a, **kw: {}
|
||||
|
||||
with mock.patch.object(storage.syncer, "sync_up") as sync_up:
|
||||
num_to_keep = 2
|
||||
checkpoint_config = CheckpointConfig(
|
||||
num_to_keep=num_to_keep, checkpoint_frequency=1
|
||||
)
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: PlacementGroupResourceManager(),
|
||||
storage=storage,
|
||||
checkpoint_period=100, # only rely on force syncing
|
||||
trial_checkpoint_config=checkpoint_config,
|
||||
)
|
||||
|
||||
class CheckpointingTrial(Trial):
|
||||
def should_checkpoint(self):
|
||||
return True
|
||||
|
||||
def get_json_state(self):
|
||||
return "", ""
|
||||
|
||||
trial = CheckpointingTrial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=checkpoint_config,
|
||||
stopping_criterion={"training_iteration": 10},
|
||||
storage=storage,
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
|
||||
# also check if the warning is printed
|
||||
buffer = []
|
||||
from ray.tune.execution.experiment_state import logger
|
||||
|
||||
with mock.patch.object(logger, "warning", lambda x: buffer.append(x)):
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert any(
|
||||
"Experiment state snapshotting has been triggered multiple times" in x
|
||||
for x in buffer
|
||||
)
|
||||
# We should sync 6 times:
|
||||
# The first checkpoint happens when the experiment starts,
|
||||
# since no checkpoints have happened yet
|
||||
# (This corresponds to the new_trial event in the runner loop)
|
||||
# Then, every num_to_keep=2 checkpoints, we should perform a forced checkpoint
|
||||
# which results in 5 more checkpoints (running for 10 iterations),
|
||||
# giving a total of 6
|
||||
assert sync_up.call_count == 6
|
||||
|
||||
|
||||
def test_checkpoint_force_by_trial_callback(ray_start_4_cpus_2_gpus_extra, tmp_path):
|
||||
"""Test that cloud syncing is forced if one of the trials has made more
|
||||
than num_to_keep checkpoints since last sync.
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::
|
||||
testCloudCheckpointForceWithNumToKeep
|
||||
"""
|
||||
|
||||
class CheckpointCallback(Callback):
|
||||
def __init__(self):
|
||||
self.num_checkpoints = 0
|
||||
|
||||
def on_trial_result(self, iteration, trials, trial: Trial, result, **info):
|
||||
# Checkpoint every two iterations
|
||||
if result[TRAINING_ITERATION] % 2 == 0:
|
||||
self.num_checkpoints += 1
|
||||
result["should_checkpoint"] = True
|
||||
|
||||
storage = mock_storage_context()
|
||||
|
||||
# disable automatic checkpointing
|
||||
checkpoint_config = CheckpointConfig(checkpoint_frequency=0)
|
||||
callback = CheckpointCallback()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=PlacementGroupResourceManager,
|
||||
storage=storage,
|
||||
callbacks=[callback],
|
||||
trial_checkpoint_config=checkpoint_config,
|
||||
)
|
||||
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=checkpoint_config,
|
||||
stopping_criterion={"training_iteration": 6},
|
||||
storage=storage,
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert callback.num_checkpoints == 3
|
||||
assert num_checkpoints(trial) == 3
|
||||
|
||||
|
||||
def test_checkpoint_sync_up_timeout(
|
||||
ray_start_4_cpus_2_gpus_extra, tmp_path, monkeypatch
|
||||
):
|
||||
"""Test that trial runner experiment checkpointing times out correctly.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::
|
||||
testForcedCloudCheckpointSyncTimeout
|
||||
"""
|
||||
storage = mock_storage_context(sync_config=ray.tune.SyncConfig(sync_timeout=0.5))
|
||||
monkeypatch.setenv("TUNE_WARN_SLOW_EXPERIMENT_CHECKPOINT_SYNC_THRESHOLD_S", "0.25")
|
||||
|
||||
def _hanging_upload_to_fs_path(*args, **kwargs):
|
||||
time.sleep(200)
|
||||
|
||||
monkeypatch.setattr(
|
||||
ray.train._internal.storage,
|
||||
"_upload_to_fs_path",
|
||||
_hanging_upload_to_fs_path,
|
||||
)
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: PlacementGroupResourceManager(),
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
# Start a hanging sync that should not block the controller
|
||||
runner.checkpoint()
|
||||
|
||||
buffer = []
|
||||
logger = logging.getLogger("ray.tune.execution.experiment_state")
|
||||
with mock.patch.object(logger, "error", lambda x, **kwargs: buffer.append(x)):
|
||||
with mock.patch.object(logger, "warning", lambda x: buffer.append(x)):
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
# We should see a log about the timeout
|
||||
assert any("Saving experiment state to storage" in x for x in buffer)
|
||||
# We should also have a warning about the slow upload
|
||||
assert any("may be a performance bottleneck" in x for x in buffer)
|
||||
|
||||
|
||||
def test_checkpoint_sync_up_error(ray_start_4_cpus_2_gpus_extra, tmp_path, monkeypatch):
|
||||
"""Test that trial runner experiment checkpointing handles errors correctly."""
|
||||
storage = mock_storage_context()
|
||||
|
||||
def _failing_upload_to_fs_path(*args, **kwargs):
|
||||
raise RuntimeError("Upload failing...")
|
||||
|
||||
monkeypatch.setattr(
|
||||
ray.train._internal.storage,
|
||||
"_upload_to_fs_path",
|
||||
_failing_upload_to_fs_path,
|
||||
)
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: PlacementGroupResourceManager(),
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
# Launching a failing upload task should not crash the controller / main thread
|
||||
runner.checkpoint()
|
||||
|
||||
buffer = []
|
||||
logger = logging.getLogger("ray.tune.execution.experiment_state")
|
||||
with mock.patch.object(logger, "error", lambda x, **kwargs: buffer.append(x)):
|
||||
runner.checkpoint(force=True)
|
||||
|
||||
# We should see a log about the failure
|
||||
assert any("Saving experiment state to storage" in x for x in buffer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,147 @@
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import PlacementGroupFactory, register_trainable
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
STORAGE = mock_storage_context()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_stop_trial(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Stopping a trial while RUNNING or PENDING should work.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testStopTrial
|
||||
"""
|
||||
|
||||
register_mock_trainable()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(), storage=STORAGE
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 10},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 2, "GPU": 1}]),
|
||||
"config": {"sleep": 1},
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
# Wait until 2 trials started
|
||||
while counter.get("RUNNING", 0) != 2:
|
||||
runner.step()
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
assert counter.get("RUNNING", 0) == 2
|
||||
assert counter.get("PENDING", 0) == 2
|
||||
|
||||
# Stop trial that is running
|
||||
for trial in trials:
|
||||
if trial.status == Trial.RUNNING:
|
||||
runner._schedule_trial_stop(trial)
|
||||
break
|
||||
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
# Wait until the next trial started
|
||||
while counter.get("RUNNING", 0) < 2:
|
||||
runner.step()
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
assert counter.get("RUNNING", 0) == 2
|
||||
assert counter.get("TERMINATED", 0) == 1
|
||||
assert counter.get("PENDING", 0) == 1
|
||||
|
||||
# Stop trial that is pending
|
||||
for trial in trials:
|
||||
if trial.status == Trial.PENDING:
|
||||
runner._schedule_trial_stop(trial)
|
||||
break
|
||||
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
# Wait until 2 trials are running again
|
||||
while counter.get("RUNNING", 0) < 2:
|
||||
runner.step()
|
||||
counter = Counter(t.status for t in trials)
|
||||
|
||||
assert counter.get("RUNNING", 0) == 2
|
||||
assert counter.get("TERMINATED", 0) == 2
|
||||
assert counter.get("PENDING", 0) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_remove_actor_tracking(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""When we reuse actors, actors that have been requested but not started
|
||||
should not be tracked in ``_stopping_actors``.
|
||||
|
||||
When actors are re-used, we cancel original actor requests for the trial.
|
||||
If these actors haven't been alive, there won't be a stop future to be resolved,
|
||||
and thus they would remain in ``TuneController._stopping_actors`` until they
|
||||
get cleaned up after 600 seconds.
|
||||
|
||||
This test asserts that these actors are not tracked in
|
||||
``TuneController._stopping_actors`` at all.
|
||||
|
||||
We start 4 actors, and one can run at a time. Actors are re-used across trials.
|
||||
When the experiment ends, we expect that only one actor is left to track
|
||||
in ``self._stopping_trials``.
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
reuse_actors=True,
|
||||
storage=STORAGE,
|
||||
)
|
||||
|
||||
def train_fn(config):
|
||||
return 1
|
||||
|
||||
register_trainable("test_remove_actor_tracking", train_fn)
|
||||
|
||||
kwargs = {
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 4, "GPU": 2}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [Trial("test_remove_actor_tracking", **kwargs) for i in range(4)]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
# Only one actor should be left to stop
|
||||
assert len(runner._stopping_actors) == 1
|
||||
|
||||
runner.cleanup()
|
||||
|
||||
assert len(runner._stopping_actors) == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "--reruns", "3", __file__]))
|
||||
@@ -0,0 +1,212 @@
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import CheckpointConfig, PlacementGroupFactory, TuneError
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.registry import TRAINABLE_CLASS, _global_registry
|
||||
from ray.tune.schedulers import FIFOScheduler
|
||||
from ray.tune.search import BasicVariantGenerator
|
||||
from ray.tune.tests.execution.utils import BudgetResourceManager
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
STORAGE = mock_storage_context()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
|
||||
|
||||
def create_mock_components():
|
||||
class _MockScheduler(FIFOScheduler):
|
||||
errored_trials = []
|
||||
|
||||
def on_trial_error(self, tune_controller, trial):
|
||||
self.errored_trials += [trial]
|
||||
|
||||
class _MockSearchAlg(BasicVariantGenerator):
|
||||
errored_trials = []
|
||||
|
||||
def on_trial_complete(self, trial_id, error=False, **kwargs):
|
||||
if error:
|
||||
self.errored_trials += [trial_id]
|
||||
|
||||
searchalg = _MockSearchAlg()
|
||||
scheduler = _MockScheduler()
|
||||
return searchalg, scheduler
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_invalid_trainable(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""An invalid trainable should make the trial fail on startup.
|
||||
|
||||
The controller itself should continue. Other trials should run.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testErrorHandling
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(), storage=STORAGE
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 1},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"storage": STORAGE,
|
||||
"config": {"sleep": 0.5},
|
||||
}
|
||||
_global_registry.register(TRAINABLE_CLASS, "asdf", None)
|
||||
trials = [Trial("asdf", **kwargs), Trial(MOCK_TRAINABLE_NAME, **kwargs)]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not trials[1].status == Trial.RUNNING:
|
||||
runner.step()
|
||||
assert trials[0].status == Trial.ERROR
|
||||
assert trials[1].status == Trial.RUNNING
|
||||
|
||||
|
||||
def test_overstep(ray_start_4_cpus_2_gpus_extra):
|
||||
"""Stepping when trials are finished should raise a TuneError.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testThrowOnOverstep
|
||||
"""
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = "1"
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: BudgetResourceManager({"CPU": 4}),
|
||||
storage=STORAGE,
|
||||
)
|
||||
runner.step()
|
||||
with pytest.raises(TuneError):
|
||||
runner.step()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize("max_failures_persistent", [(0, False), (1, False), (2, True)])
|
||||
def test_failure_recovery(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, max_failures_persistent
|
||||
):
|
||||
"""Test failure recover with `max_failures`.
|
||||
|
||||
Trials should be retried up to `max_failures` times.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testFailureRecoveryDisabled
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testFailureRecoveryEnabled
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testFailureRecoveryMaxFailures
|
||||
"""
|
||||
max_failures, persistent_error = max_failures_persistent
|
||||
searchalg, scheduler = create_mock_components()
|
||||
|
||||
runner = TuneController(
|
||||
search_alg=searchalg,
|
||||
scheduler=scheduler,
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"stopping_criterion": {"training_iteration": 2},
|
||||
"checkpoint_config": CheckpointConfig(checkpoint_frequency=1),
|
||||
"max_failures": max_failures,
|
||||
"config": {"mock_error": True, "persistent_error": persistent_error},
|
||||
"storage": STORAGE,
|
||||
}
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
trials = runner.get_trials()
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
if persistent_error or not max_failures:
|
||||
assert trials[0].status == Trial.ERROR
|
||||
|
||||
num_failures = max_failures + 1
|
||||
assert trials[0].num_failures == num_failures
|
||||
# search alg receives on_complete, so only after the max failures
|
||||
# have been exhausted. Thus, it only has errored_trials if the
|
||||
# trial fails even in the last try.
|
||||
assert len(searchalg.errored_trials) == 1
|
||||
# search alg receives on_error, so every failure is registered.
|
||||
assert len(scheduler.errored_trials) == num_failures
|
||||
else:
|
||||
assert trials[0].status == Trial.TERMINATED
|
||||
assert trials[0].num_failures == 1
|
||||
assert len(searchalg.errored_trials) == 0
|
||||
assert len(scheduler.errored_trials) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize("fail_fast", [True, TuneController.RAISE])
|
||||
def test_fail_fast(ray_start_4_cpus_2_gpus_extra, resource_manager_cls, fail_fast):
|
||||
"""Test fail_fast feature.
|
||||
|
||||
If fail_fast=True, after the first failure, all other trials should be terminated
|
||||
(because we end the experiment).
|
||||
|
||||
If fail_fast=RAISE, after the first failure, we should raise an error.
|
||||
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testFailFast
|
||||
Legacy test: test_trial_runner_2.py::TrialRunnerTest::testFailFastRaise
|
||||
"""
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
fail_fast=fail_fast,
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"checkpoint_config": CheckpointConfig(checkpoint_frequency=1),
|
||||
"max_failures": 0,
|
||||
"config": {
|
||||
"mock_error": True,
|
||||
"persistent_error": True,
|
||||
},
|
||||
"storage": STORAGE,
|
||||
}
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
runner.add_trial(Trial(MOCK_TRAINABLE_NAME, **kwargs))
|
||||
trials = runner.get_trials()
|
||||
|
||||
if fail_fast == TuneController.RAISE:
|
||||
with pytest.raises(Exception):
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
runner.cleanup()
|
||||
return
|
||||
else:
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
status_count = Counter(t.status for t in trials)
|
||||
|
||||
# One trial failed
|
||||
assert status_count.get(Trial.ERROR) == 1
|
||||
# The other one was pre-empted
|
||||
assert status_count.get(Trial.TERMINATED) == 1
|
||||
|
||||
# Controller finished
|
||||
with pytest.raises(TuneError):
|
||||
runner.step()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,278 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray import tune
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import PlacementGroupFactory, TuneError
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.schedulers import FIFOScheduler, TrialScheduler
|
||||
from ray.tune.search import BasicVariantGenerator
|
||||
from ray.tune.utils.mock import TrialStatusSnapshot, TrialStatusSnapshotTaker
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
STORAGE = mock_storage_context()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"bundles",
|
||||
[
|
||||
[{"CPU": 1}, {"CPU": 3, "GPU": 1}],
|
||||
[{"CPU": 1, "a": 2}],
|
||||
[{"CPU": 1}, {"a": 2}],
|
||||
[{"CPU": 1, "GPU": 1}, {"GPU": 1}],
|
||||
],
|
||||
)
|
||||
def test_resource_parallelism_single(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, bundles
|
||||
):
|
||||
"""Test that extra and custom resources are respected for parallelism.
|
||||
|
||||
We schedule two trials with resources according to the bundle. If only
|
||||
the head bundle or only CPU/GPU resources were considered, both trials
|
||||
could run in parallel.
|
||||
|
||||
However, we assert that the resources in child bundles and extra resources
|
||||
are respected and only one trial runs in parallel.
|
||||
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testExtraResources
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testCustomResources
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testExtraCustomResources
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testResourceScheduler
|
||||
"""
|
||||
snapshot = TrialStatusSnapshot()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
callbacks=[TrialStatusSnapshotTaker(snapshot)],
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 1},
|
||||
"placement_group_factory": PlacementGroupFactory(bundles),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert snapshot.max_running_trials() == 1
|
||||
assert snapshot.all_trials_are_terminated()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_fractional_gpus(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Test that fractional GPUs lead to more parallelism.
|
||||
|
||||
We schedule four trials with 0.75 GPUs each. Since our cluster has 2 GPUs,
|
||||
we should be able to run 2 trials in parallel.
|
||||
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testFractionalGpus
|
||||
"""
|
||||
snapshot = TrialStatusSnapshot()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
callbacks=[TrialStatusSnapshotTaker(snapshot)],
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 1},
|
||||
"placement_group_factory": PlacementGroupFactory([{"GPU": 0.75}]),
|
||||
"config": {
|
||||
"sleep": 1,
|
||||
},
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [Trial(MOCK_TRAINABLE_NAME, **kwargs) for i in range(4)]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert snapshot.max_running_trials() == 2
|
||||
assert snapshot.all_trials_are_terminated()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_multi_step(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Test that trials can run for more than one iteration.
|
||||
|
||||
Todo (krfricke): This is not a resource test, so it should be moved.
|
||||
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testMultiStepRun
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testMultiStepRun2
|
||||
"""
|
||||
snapshot = TrialStatusSnapshot()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
callbacks=[TrialStatusSnapshotTaker(snapshot)],
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 5},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 1}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [Trial(MOCK_TRAINABLE_NAME, **kwargs) for i in range(2)]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
# Overstepping should throw error
|
||||
# test_trial_runner.py::TrialRunnerTest::testMultiStepRun2
|
||||
with pytest.raises(TuneError):
|
||||
runner.step()
|
||||
|
||||
assert snapshot.all_trials_are_terminated()
|
||||
assert all(t.last_result["training_iteration"] == 5 for t in runner.get_trials())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_resources_changing(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Checks that resource requirements can be changed on fly.
|
||||
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testChangeResources
|
||||
"""
|
||||
|
||||
class ChangingScheduler(FIFOScheduler):
|
||||
def on_trial_result(self, tune_controller, trial, result):
|
||||
if result["training_iteration"] == 1:
|
||||
# NOTE: This is a hack to get around the new pausing logic,
|
||||
# which doesn't set the trial status to PAUSED immediately.
|
||||
orig_status = trial.status
|
||||
trial.set_status(Trial.PAUSED)
|
||||
trial.update_resources(dict(cpu=4, gpu=0))
|
||||
trial.set_status(orig_status)
|
||||
return TrialScheduler.PAUSE
|
||||
return TrialScheduler.NOOP
|
||||
|
||||
scheduler = ChangingScheduler()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
scheduler=scheduler,
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 2},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 2, "GPU": 0}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [Trial(MOCK_TRAINABLE_NAME, **kwargs)]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not trials[0].status == Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].status == Trial.RUNNING
|
||||
assert runner._actor_manager.get_live_actors_resources().get("CPU") == 2
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
trials[0].update_resources(dict(cpu=4, gpu=0))
|
||||
|
||||
while trials[0].status == Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].status == Trial.PAUSED
|
||||
|
||||
while not trials[0].status == Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert runner._actor_manager.get_live_actors_resources().get("CPU") == 4
|
||||
|
||||
runner.step()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_queue_filling(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Checks that the trial queue is filled even if only 1 pending trial is allowed.
|
||||
|
||||
Legacy test: test_trial_runner.py::TrialRunnerTest::testQueueFilling
|
||||
"""
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = "1"
|
||||
|
||||
def f1(config):
|
||||
for i in range(10):
|
||||
yield i
|
||||
time.sleep(1)
|
||||
|
||||
tune.register_trainable("f1", f1)
|
||||
|
||||
search_alg = BasicVariantGenerator()
|
||||
search_alg.add_configurations(
|
||||
{
|
||||
"foo": {
|
||||
"run": "f1",
|
||||
"num_samples": 100,
|
||||
"config": {
|
||||
"a": tune.sample_from(lambda spec: 5.0 / 7),
|
||||
"b": tune.sample_from(lambda spec: "long" * 40),
|
||||
},
|
||||
"resources_per_trial": {"cpu": 2},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=search_alg,
|
||||
storage=STORAGE,
|
||||
)
|
||||
|
||||
while len(runner.get_trials()) < 3:
|
||||
runner.step()
|
||||
|
||||
# All trials are enqueued
|
||||
assert len(runner.get_trials()) == 3
|
||||
|
||||
status_count = Counter(t.status for t in runner.get_trials())
|
||||
while status_count.get(Trial.RUNNING, 0) < 2 and not runner.is_finished():
|
||||
runner.step()
|
||||
status_count = Counter(t.status for t in runner.get_trials())
|
||||
|
||||
assert len(runner.get_trials()) == 3
|
||||
|
||||
status_count = Counter(t.status for t in runner.get_trials())
|
||||
assert status_count.get(Trial.RUNNING) == 2
|
||||
assert status_count.get(Trial.PENDING) == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "--reruns", "3", __file__]))
|
||||
@@ -0,0 +1,525 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray import tune
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import CheckpointConfig, Experiment, PlacementGroupFactory, ResumeConfig
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.impl.placeholder import create_resolvers_map, inject_placeholders
|
||||
from ray.tune.search import BasicVariantGenerator
|
||||
from ray.tune.utils.mock_trainable import (
|
||||
MOCK_ERROR_KEY,
|
||||
MOCK_TRAINABLE_NAME,
|
||||
register_mock_trainable,
|
||||
)
|
||||
|
||||
STORAGE = mock_storage_context()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_dataset_references(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Check that references to Ray Datasets are replaced on resume.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::
|
||||
testSearcherCorrectReferencesAfterRestore
|
||||
"""
|
||||
|
||||
class FakeDataset:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
config = {
|
||||
"param1": {
|
||||
"param2": tune.grid_search(
|
||||
[FakeDataset("1"), FakeDataset("2"), FakeDataset("3")]
|
||||
),
|
||||
},
|
||||
"param4": tune.sample_from(lambda: 1),
|
||||
"param5": tune.sample_from(lambda spec: spec.config["param1"]["param2"]),
|
||||
}
|
||||
resolvers = create_resolvers_map()
|
||||
config = inject_placeholders(config, resolvers)
|
||||
|
||||
def create_searcher():
|
||||
search_alg = BasicVariantGenerator()
|
||||
experiment_spec = {
|
||||
"run": MOCK_TRAINABLE_NAME,
|
||||
"stop": {"training_iteration": 2},
|
||||
"config": config,
|
||||
}
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
search_alg.add_configurations(experiments)
|
||||
return search_alg
|
||||
|
||||
searcher = create_searcher()
|
||||
|
||||
restored_config = {
|
||||
"param1": {
|
||||
"param2": tune.grid_search(
|
||||
[FakeDataset("4"), FakeDataset("5"), FakeDataset("6")]
|
||||
),
|
||||
},
|
||||
"param4": tune.sample_from(lambda: 8),
|
||||
"param5": tune.sample_from(lambda spec: spec["config"]["param1"]["param2"]),
|
||||
}
|
||||
replaced_resolvers = create_resolvers_map()
|
||||
inject_placeholders(restored_config, replaced_resolvers)
|
||||
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
reuse_actors=False,
|
||||
search_alg=searcher,
|
||||
placeholder_resolvers=replaced_resolvers,
|
||||
checkpoint_period=-1,
|
||||
storage=STORAGE,
|
||||
)
|
||||
|
||||
while len(runner.get_trials()) < 3 or any(
|
||||
trial.status not in {Trial.RUNNING, Trial.TERMINATED}
|
||||
for trial in runner.get_trials()
|
||||
):
|
||||
runner.step()
|
||||
|
||||
assert len(runner.get_trials()) == 3, [t.config for t in runner.get_trials()]
|
||||
for t in runner.get_trials():
|
||||
# Make sure that all the trials carry updated config values.
|
||||
assert t.config["param1"]["param2"].name in ["4", "5", "6"]
|
||||
assert t.config["param4"] == 8
|
||||
assert t.config["param5"].name in ["4", "5", "6"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_no_error_resume(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Check that `resume=True` does not resume errored trials.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testTrialErrorResumeFalse
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 4},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 0}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [
|
||||
Trial(MOCK_TRAINABLE_NAME, config={MOCK_ERROR_KEY: True}, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
assert trials[0].status == Trial.ERROR
|
||||
del runner
|
||||
|
||||
new_runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
resume_config=ResumeConfig(
|
||||
unfinished=ResumeConfig.ResumeType.RESUME,
|
||||
errored=ResumeConfig.ResumeType.SKIP,
|
||||
finished=ResumeConfig.ResumeType.SKIP,
|
||||
),
|
||||
)
|
||||
|
||||
assert len(new_runner.get_trials()) == 3
|
||||
assert Trial.ERROR in (t.status for t in new_runner.get_trials())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_error_only_resume(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Check that `resume=ERRORED_ONLY` only resumes errored trials.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testTrialErrorResumeTrue
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
kwargs = {
|
||||
"stopping_criterion": {"training_iteration": 4},
|
||||
"placement_group_factory": PlacementGroupFactory([{"CPU": 1, "GPU": 0}]),
|
||||
"storage": STORAGE,
|
||||
}
|
||||
trials = [
|
||||
Trial(MOCK_TRAINABLE_NAME, config={MOCK_ERROR_KEY: True}, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
Trial(MOCK_TRAINABLE_NAME, **kwargs),
|
||||
]
|
||||
for t in trials:
|
||||
runner.add_trial(t)
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
assert trials[0].status == Trial.ERROR
|
||||
del runner
|
||||
|
||||
new_runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
resume_config=ResumeConfig(
|
||||
unfinished=ResumeConfig.ResumeType.SKIP,
|
||||
errored=ResumeConfig.ResumeType.RESUME,
|
||||
finished=ResumeConfig.ResumeType.SKIP,
|
||||
),
|
||||
)
|
||||
|
||||
assert len(new_runner.get_trials()) == 3
|
||||
assert Trial.ERROR not in (t.status for t in new_runner.get_trials())
|
||||
# The below is just a check for standard behavior.
|
||||
disable_error = False
|
||||
for t in new_runner.get_trials():
|
||||
if t.config.get(MOCK_ERROR_KEY):
|
||||
t.config[MOCK_ERROR_KEY] = False
|
||||
disable_error = True
|
||||
assert disable_error
|
||||
|
||||
while not new_runner.is_finished():
|
||||
new_runner.step()
|
||||
assert Trial.ERROR not in (t.status for t in new_runner.get_trials())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_trial_save_restore(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Creates different trials to test runner.checkpoint/restore.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testTrialSaveRestore
|
||||
"""
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
checkpoint_period=0,
|
||||
storage=STORAGE,
|
||||
)
|
||||
trials = [
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="trial_terminate",
|
||||
stopping_criterion={"training_iteration": 1},
|
||||
checkpoint_config=CheckpointConfig(checkpoint_frequency=1),
|
||||
storage=STORAGE,
|
||||
)
|
||||
]
|
||||
runner.add_trial(trials[0])
|
||||
while not runner.is_finished():
|
||||
# Start trial, process result, dispatch save and process save.
|
||||
runner.step()
|
||||
assert trials[0].status == Trial.TERMINATED
|
||||
|
||||
trials += [
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="trial_fail",
|
||||
stopping_criterion={"training_iteration": 3},
|
||||
checkpoint_config=CheckpointConfig(checkpoint_frequency=1),
|
||||
config={MOCK_ERROR_KEY: True},
|
||||
storage=STORAGE,
|
||||
)
|
||||
]
|
||||
runner.add_trial(trials[1])
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
assert trials[1].status == Trial.ERROR
|
||||
|
||||
trials += [
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="trial_succ",
|
||||
stopping_criterion={"training_iteration": 2},
|
||||
checkpoint_config=CheckpointConfig(checkpoint_frequency=1),
|
||||
storage=STORAGE,
|
||||
)
|
||||
]
|
||||
runner.add_trial(trials[2])
|
||||
|
||||
while not trials[2].status == Trial.RUNNING:
|
||||
runner.step() # Start trial
|
||||
assert len(runner._get_trial_checkpoints()) == 3
|
||||
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
runner2 = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
resume_config=ResumeConfig(
|
||||
unfinished=ResumeConfig.ResumeType.RESUME,
|
||||
errored=ResumeConfig.ResumeType.SKIP,
|
||||
finished=ResumeConfig.ResumeType.SKIP,
|
||||
),
|
||||
)
|
||||
for tid in ["trial_terminate", "trial_fail"]:
|
||||
original_trial = runner.get_trial(tid)
|
||||
restored_trial = runner2.get_trial(tid)
|
||||
assert original_trial.status == restored_trial.status
|
||||
|
||||
restored_trial = runner2.get_trial("trial_succ")
|
||||
assert Trial.PENDING == restored_trial.status
|
||||
|
||||
while not runner2.is_finished():
|
||||
runner2.step()
|
||||
assert restored_trial.status == Trial.TERMINATED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_trial_no_checkpoint_save(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Check that non-checkpointing trials *are* saved.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testTrialNoCheckpointSave
|
||||
"""
|
||||
with patch.dict(os.environ, {"TUNE_MAX_PENDING_TRIALS_PG": "1"}):
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
checkpoint_period=0,
|
||||
storage=STORAGE,
|
||||
)
|
||||
|
||||
runner.add_trial(
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="non_checkpoint",
|
||||
stopping_criterion={"training_iteration": 2},
|
||||
storage=STORAGE,
|
||||
)
|
||||
)
|
||||
|
||||
while not all(t.status == Trial.TERMINATED for t in runner.get_trials()):
|
||||
runner.step()
|
||||
|
||||
runner.add_trial(
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="checkpoint",
|
||||
checkpoint_config=CheckpointConfig(
|
||||
checkpoint_at_end=True,
|
||||
),
|
||||
stopping_criterion={"training_iteration": 2},
|
||||
storage=STORAGE,
|
||||
)
|
||||
)
|
||||
|
||||
while not all(t.status == Trial.TERMINATED for t in runner.get_trials()):
|
||||
runner.step()
|
||||
|
||||
runner.add_trial(
|
||||
Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
trial_id="pending",
|
||||
stopping_criterion={"training_iteration": 2},
|
||||
storage=STORAGE,
|
||||
)
|
||||
)
|
||||
|
||||
old_trials = runner.get_trials()
|
||||
while not old_trials[2].has_reported_at_least_once:
|
||||
runner.step()
|
||||
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
runner2 = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
resume_config=ResumeConfig(
|
||||
unfinished=ResumeConfig.ResumeType.RESUME,
|
||||
errored=ResumeConfig.ResumeType.SKIP,
|
||||
finished=ResumeConfig.ResumeType.SKIP,
|
||||
),
|
||||
)
|
||||
new_trials = runner2.get_trials()
|
||||
assert len(new_trials) == 3
|
||||
assert runner2.get_trial("non_checkpoint").status == Trial.TERMINATED
|
||||
assert runner2.get_trial("checkpoint").status == Trial.TERMINATED
|
||||
assert runner2.get_trial("pending").status == Trial.PENDING
|
||||
assert runner2.get_trial("pending").has_reported_at_least_once
|
||||
runner2.step()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_checkpoint_overwrite(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls
|
||||
):
|
||||
"""Check that experiment state checkpoint are not overwritten on continue.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testCheckpointOverwrite
|
||||
"""
|
||||
storage = mock_storage_context()
|
||||
|
||||
def count_checkpoints(cdir):
|
||||
return sum(
|
||||
(fname.startswith("experiment_state") and fname.endswith(".json"))
|
||||
for fname in os.listdir(cdir)
|
||||
)
|
||||
|
||||
tmpdir = storage.experiment_driver_staging_path
|
||||
# The Trial `local_dir` must match the TrialRunner `local_checkpoint_dir`
|
||||
# to match the directory structure assumed by `TrialRunner.resume`.
|
||||
# See `test_trial_runner2.TrialRunnerTest2.testPauseResumeCheckpointCount`
|
||||
# for more details.
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
checkpoint_config=CheckpointConfig(checkpoint_frequency=1),
|
||||
storage=storage,
|
||||
)
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=storage,
|
||||
checkpoint_period=0,
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
while not trial.status == Trial.RUNNING:
|
||||
runner.step()
|
||||
# force checkpoint
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
# Only one experiment state file
|
||||
assert count_checkpoints(tmpdir) == 1
|
||||
|
||||
runner2 = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=storage,
|
||||
resume_config=ResumeConfig(
|
||||
unfinished=ResumeConfig.ResumeType.RESUME,
|
||||
errored=ResumeConfig.ResumeType.SKIP,
|
||||
finished=ResumeConfig.ResumeType.SKIP,
|
||||
),
|
||||
)
|
||||
trial = runner2.get_trials()[0]
|
||||
while not trial.status == Trial.RUNNING:
|
||||
runner2.step()
|
||||
# After resume, we have a new experiment state file in the directory
|
||||
assert count_checkpoints(tmpdir) == 2
|
||||
|
||||
runner2.checkpoint()
|
||||
assert count_checkpoints(tmpdir) == 2
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO(justinvyu): Data lineage serialization context is broken.")
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_controller_restore_with_dataset(
|
||||
ray_start_4_cpus_2_gpus_extra, resource_manager_cls, tmp_path
|
||||
):
|
||||
"""Test trial runner checkpointing where trials contain Datasets.
|
||||
When possible, a dataset plan should be saved (for read_* APIs).
|
||||
See `Dataset.serialize_lineage` for more information.
|
||||
|
||||
If a dataset cannot be serialized, an experiment checkpoint
|
||||
should still be created. Users can pass in the dataset again by
|
||||
re-specifying the `param_space`.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::
|
||||
testExperimentCheckpointWithDatasets
|
||||
"""
|
||||
# Save some test data to load
|
||||
data_filepath = os.path.join(tmp_path, "test.csv")
|
||||
pd.DataFrame({"x": list(range(10))}).to_csv(data_filepath)
|
||||
|
||||
def create_trial_config():
|
||||
return {
|
||||
"datasets": {
|
||||
"with_lineage": ray.data.read_csv(data_filepath),
|
||||
"no_lineage": ray.data.from_items([{"x": i} for i in range(10)]),
|
||||
}
|
||||
}
|
||||
|
||||
resolvers = create_resolvers_map()
|
||||
config_with_placeholders = inject_placeholders(create_trial_config(), resolvers)
|
||||
trial = Trial(
|
||||
MOCK_TRAINABLE_NAME,
|
||||
config=config_with_placeholders,
|
||||
storage=STORAGE,
|
||||
)
|
||||
trial.init_local_path()
|
||||
runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
placeholder_resolvers=resolvers,
|
||||
)
|
||||
runner.add_trial(trial)
|
||||
# Req: TrialRunner checkpointing shouldn't error
|
||||
runner.checkpoint(force=True, wait=True)
|
||||
|
||||
# Manually clear all block refs that may have been created
|
||||
ray.shutdown()
|
||||
ray.init(num_cpus=2)
|
||||
|
||||
register_mock_trainable()
|
||||
new_runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
)
|
||||
new_runner.resume(resume_config=ResumeConfig())
|
||||
[loaded_trial] = new_runner.get_trials()
|
||||
loaded_datasets = loaded_trial.config["datasets"]
|
||||
|
||||
# Req: The deserialized dataset (w/ lineage) should be usable.
|
||||
assert [el["x"] for el in loaded_datasets["with_lineage"].take()] == list(range(10))
|
||||
|
||||
replaced_resolvers = create_resolvers_map()
|
||||
inject_placeholders(create_trial_config(), replaced_resolvers)
|
||||
|
||||
respecified_config_runner = TuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
storage=STORAGE,
|
||||
placeholder_resolvers=replaced_resolvers,
|
||||
)
|
||||
respecified_config_runner.resume(resume_config=ResumeConfig())
|
||||
[loaded_trial] = respecified_config_runner.get_trials()
|
||||
ray_ds_no_lineage = loaded_trial.config["datasets"]["no_lineage"]
|
||||
|
||||
# Req: The dataset (w/o lineage) can be re-specified and is usable after.
|
||||
assert [el["x"] for el in ray_ds_no_lineage.take()] == list(range(10))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,431 @@
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.constants import TRAINING_ITERATION
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune import Experiment, PlacementGroupFactory
|
||||
from ray.tune.execution.tune_controller import TuneController, _get_max_pending_trials
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.schedulers import FIFOScheduler, TrialScheduler
|
||||
from ray.tune.search import ConcurrencyLimiter, Repeater, Searcher, SearchGenerator
|
||||
from ray.tune.search._mock import _MockSearcher, _MockSuggestionAlgorithm
|
||||
from ray.tune.utils.mock_trainable import MOCK_TRAINABLE_NAME, register_mock_trainable
|
||||
|
||||
|
||||
class TestTuneController(TuneController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.update(dict(storage=mock_storage_context()))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_test_trainable():
|
||||
register_mock_trainable()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_8_cpus():
|
||||
address_info = ray.init(num_cpus=8, num_gpus=0)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_4_cpus_2_gpus_extra():
|
||||
address_info = ray.init(num_cpus=4, num_gpus=2, resources={"a": 2})
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_search_alg_notification(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Check that the searchers gets notified of trial results + completions.
|
||||
|
||||
Also check that the searcher is "finished" before the runner, i.e. the runner
|
||||
continues processing trials when the searcher finished.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearchAlgNotification
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearchAlgFinished
|
||||
"""
|
||||
|
||||
experiment_spec = {"run": MOCK_TRAINABLE_NAME, "stop": {"training_iteration": 2}}
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
search_alg = _MockSuggestionAlgorithm()
|
||||
searcher = search_alg.searcher
|
||||
search_alg.add_configurations(experiments)
|
||||
|
||||
runner = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(), search_alg=search_alg
|
||||
)
|
||||
|
||||
# Run until trial is running
|
||||
while not search_alg.is_finished():
|
||||
runner.step()
|
||||
|
||||
trials = runner.get_trials()
|
||||
|
||||
# Make sure trial started
|
||||
while trials[0].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert trials[0].status == Trial.RUNNING
|
||||
assert search_alg.is_finished()
|
||||
assert not runner.is_finished()
|
||||
|
||||
# Run until everything finished
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
assert trials[0].status == Trial.TERMINATED
|
||||
assert search_alg.is_finished()
|
||||
assert runner.is_finished()
|
||||
|
||||
assert searcher.counter["result"] == 1
|
||||
assert searcher.counter["complete"] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_search_alg_scheduler_stop(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Check that a scheduler-issued stop also notifies the search algorithm.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearchAlgSchedulerInteraction # noqa
|
||||
"""
|
||||
|
||||
class _MockScheduler(FIFOScheduler):
|
||||
def on_trial_result(self, *args, **kwargs):
|
||||
return TrialScheduler.STOP
|
||||
|
||||
experiment_spec = {"run": MOCK_TRAINABLE_NAME, "stop": {"training_iteration": 5}}
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
search_alg = _MockSuggestionAlgorithm()
|
||||
searcher = search_alg.searcher
|
||||
search_alg.add_configurations(experiments)
|
||||
|
||||
runner = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=search_alg,
|
||||
scheduler=_MockScheduler(),
|
||||
)
|
||||
|
||||
trials = runner.get_trials()
|
||||
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
|
||||
# Result is not processed because trial stop takes precedence
|
||||
assert searcher.counter["result"] == 0
|
||||
# But on_trial_complete is triggered...
|
||||
assert searcher.counter["complete"] == 1
|
||||
# ... and still updates the last result.
|
||||
assert trials[0].last_result[TRAINING_ITERATION] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_search_alg_stalled(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Checks that runner and searcher state is maintained when stalled.
|
||||
|
||||
We use a concurrency limit of 1, meaning each trial is added one-by-one
|
||||
from the searchers.
|
||||
|
||||
We then run three samples. During the second trial, we stall the searcher,
|
||||
which means we don't suggest new trials after it finished.
|
||||
|
||||
In this case, the runner should still be considered "running". Once we unstall,
|
||||
the experiment finishes regularly.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearchAlgStalled
|
||||
"""
|
||||
experiment_spec = {
|
||||
"run": MOCK_TRAINABLE_NAME,
|
||||
"num_samples": 3,
|
||||
"stop": {"training_iteration": 1},
|
||||
}
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
search_alg = _MockSuggestionAlgorithm(max_concurrent=1)
|
||||
search_alg.add_configurations(experiments)
|
||||
searcher = search_alg.searcher
|
||||
runner = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=search_alg,
|
||||
)
|
||||
runner.step()
|
||||
trials = runner.get_trials()
|
||||
while trials[0].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
# On next step, trials[1] is created
|
||||
runner.step()
|
||||
|
||||
trials = runner.get_trials()
|
||||
|
||||
while trials[1].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert trials[1].status == Trial.RUNNING
|
||||
assert len(searcher.live_trials) == 1
|
||||
|
||||
# Stall: We don't suggest new algorithms
|
||||
searcher.stall = True
|
||||
|
||||
while trials[1].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
assert trials[1].status == Trial.TERMINATED
|
||||
assert len(searcher.live_trials) == 0
|
||||
|
||||
assert all(trial.is_finished() for trial in trials)
|
||||
assert not search_alg.is_finished()
|
||||
assert not runner.is_finished()
|
||||
|
||||
# Unstall
|
||||
searcher.stall = False
|
||||
|
||||
# Create trials[2]
|
||||
runner.step()
|
||||
|
||||
trials = runner.get_trials()
|
||||
|
||||
while trials[2].status != Trial.RUNNING:
|
||||
runner.step()
|
||||
|
||||
assert trials[2].status == Trial.RUNNING
|
||||
assert len(searcher.live_trials) == 1
|
||||
|
||||
while trials[2].status != Trial.TERMINATED:
|
||||
runner.step()
|
||||
|
||||
assert len(searcher.live_trials) == 0
|
||||
assert search_alg.is_finished()
|
||||
assert runner.is_finished()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_search_alg_finishes(ray_start_4_cpus_2_gpus_extra, resource_manager_cls):
|
||||
"""Empty SearchAlg changing state in `next_trials` does not crash.
|
||||
|
||||
The search algorithm changes to ``finished`` mid-run. This should not
|
||||
affect processing of the experiment.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearchAlgFinishes
|
||||
"""
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = "1"
|
||||
|
||||
class FinishFastAlg(_MockSuggestionAlgorithm):
|
||||
_index = 0
|
||||
|
||||
def next_trial(self):
|
||||
spec = self._experiment.spec
|
||||
trial = None
|
||||
if self._index < spec["num_samples"]:
|
||||
trial = Trial(
|
||||
spec.get("run"),
|
||||
stopping_criterion=spec.get("stop"),
|
||||
storage=spec.get("storage"),
|
||||
)
|
||||
self._index += 1
|
||||
|
||||
if self._index > 4:
|
||||
self.set_finished()
|
||||
|
||||
return trial
|
||||
|
||||
def suggest(self, trial_id):
|
||||
return {}
|
||||
|
||||
experiment_spec = {
|
||||
"run": MOCK_TRAINABLE_NAME,
|
||||
"num_samples": 2,
|
||||
"stop": {"training_iteration": 1},
|
||||
}
|
||||
searcher = FinishFastAlg()
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
searcher.add_configurations(experiments)
|
||||
|
||||
runner = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=searcher,
|
||||
)
|
||||
|
||||
assert not runner.is_finished()
|
||||
|
||||
while len(runner.get_trials()) < 2:
|
||||
runner.step() # Launch 2 runs
|
||||
|
||||
assert not searcher.is_finished()
|
||||
assert not runner.is_finished()
|
||||
|
||||
searcher_finished_before = False
|
||||
while not runner.is_finished():
|
||||
runner.step()
|
||||
searcher_finished_before = searcher.is_finished()
|
||||
|
||||
# searcher_finished_before will be True if the searcher was finished before
|
||||
# the controller.
|
||||
assert searcher_finished_before
|
||||
|
||||
|
||||
# Todo (krfricke): Fix in next batch
|
||||
@pytest.mark.skip("This test is currently flaky as it can fail due to timing issues.")
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_searcher_save_restore(ray_start_8_cpus, resource_manager_cls, tmpdir):
|
||||
"""Searchers state should be saved and restored in the experiment checkpoint.
|
||||
|
||||
Legacy test: test_trial_runner_3.py::TrialRunnerTest::testSearcherSaveRestore
|
||||
"""
|
||||
|
||||
def create_searcher():
|
||||
class TestSuggestion(Searcher):
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
self.returned_result = []
|
||||
super().__init__(metric="episode_reward_mean", mode="max")
|
||||
|
||||
def suggest(self, trial_id):
|
||||
self.index += 1
|
||||
return {"test_variable": self.index}
|
||||
|
||||
def on_trial_complete(self, trial_id, result=None, **kwargs):
|
||||
self.returned_result.append(result)
|
||||
|
||||
def save(self, checkpoint_path):
|
||||
with open(checkpoint_path, "wb") as f:
|
||||
pickle.dump(self.__dict__, f)
|
||||
|
||||
def restore(self, checkpoint_path):
|
||||
with open(checkpoint_path, "rb") as f:
|
||||
self.__dict__.update(pickle.load(f))
|
||||
|
||||
searcher = TestSuggestion(0)
|
||||
searcher = ConcurrencyLimiter(searcher, max_concurrent=2)
|
||||
searcher = Repeater(searcher, repeat=3, set_index=False)
|
||||
search_alg = SearchGenerator(searcher)
|
||||
experiment_spec = {
|
||||
"run": MOCK_TRAINABLE_NAME,
|
||||
"num_samples": 20,
|
||||
"config": {"sleep": 10},
|
||||
"stop": {"training_iteration": 2},
|
||||
"resources_per_trial": PlacementGroupFactory([{"CPU": 1}]),
|
||||
}
|
||||
experiments = [Experiment.from_json("test", experiment_spec)]
|
||||
search_alg.add_configurations(experiments)
|
||||
return search_alg
|
||||
|
||||
searcher = create_searcher()
|
||||
|
||||
runner = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=searcher,
|
||||
checkpoint_period=-1,
|
||||
experiment_path=str(tmpdir),
|
||||
)
|
||||
|
||||
while len(runner.get_trials()) < 6:
|
||||
runner.step()
|
||||
|
||||
assert len(runner.get_trials()) == 6, [t.config for t in runner.get_trials()]
|
||||
runner.checkpoint()
|
||||
trials = runner.get_trials()
|
||||
[runner._schedule_trial_stop(t) for t in trials if t.status is not Trial.ERROR]
|
||||
|
||||
runner.cleanup()
|
||||
|
||||
del runner
|
||||
|
||||
searcher = create_searcher()
|
||||
|
||||
runner2 = TestTuneController(
|
||||
resource_manager_factory=lambda: resource_manager_cls(),
|
||||
search_alg=searcher,
|
||||
experiment_path=str(tmpdir),
|
||||
resume="LOCAL",
|
||||
)
|
||||
|
||||
assert len(runner2.get_trials()) == 6, [t.config for t in runner2.get_trials()]
|
||||
|
||||
def trial_statuses():
|
||||
return [t.status for t in runner2.get_trials()]
|
||||
|
||||
def num_running_trials():
|
||||
return sum(t.status == Trial.RUNNING for t in runner2.get_trials())
|
||||
|
||||
while num_running_trials() < 6:
|
||||
runner2.step()
|
||||
|
||||
assert len(set(trial_statuses())) == 1
|
||||
assert Trial.RUNNING in trial_statuses()
|
||||
|
||||
for i in range(20):
|
||||
runner2.step()
|
||||
assert 1 <= num_running_trials() <= 6
|
||||
|
||||
evaluated = [t.evaluated_params["test_variable"] for t in runner2.get_trials()]
|
||||
count = Counter(evaluated)
|
||||
assert all(v <= 3 for v in count.values())
|
||||
|
||||
|
||||
class TestGetMaxPendingTrials:
|
||||
"""Tests for _get_max_pending_trials with custom searchers."""
|
||||
|
||||
def setup_method(self):
|
||||
self._orig = os.environ.pop("TUNE_MAX_PENDING_TRIALS_PG", None)
|
||||
|
||||
def teardown_method(self):
|
||||
if self._orig is not None:
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = self._orig
|
||||
else:
|
||||
os.environ.pop("TUNE_MAX_PENDING_TRIALS_PG", None)
|
||||
|
||||
def test_env_var_override(self):
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = "42"
|
||||
sg = SearchGenerator(_MockSearcher())
|
||||
assert _get_max_pending_trials(sg) == 42
|
||||
|
||||
def test_search_generator_without_concurrency_limiter(self):
|
||||
sg = SearchGenerator(_MockSearcher())
|
||||
assert _get_max_pending_trials(sg) == 1
|
||||
|
||||
def test_search_generator_with_concurrency_limiter(self):
|
||||
limited = ConcurrencyLimiter(_MockSearcher(), max_concurrent=8)
|
||||
sg = SearchGenerator(limited)
|
||||
assert _get_max_pending_trials(sg) == 8
|
||||
|
||||
def test_search_generator_with_nested_concurrency_limiter(self):
|
||||
limited = ConcurrencyLimiter(_MockSearcher(), max_concurrent=8)
|
||||
repeater = Repeater(limited, repeat=3, set_index=False)
|
||||
sg = SearchGenerator(repeater)
|
||||
assert _get_max_pending_trials(sg) == 8
|
||||
|
||||
@pytest.mark.parametrize("max_concurrent", [1, 4, 16])
|
||||
def test_various_concurrency_values(self, max_concurrent):
|
||||
limited = ConcurrencyLimiter(_MockSearcher(), max_concurrent=max_concurrent)
|
||||
sg = SearchGenerator(limited)
|
||||
assert _get_max_pending_trials(sg) == max_concurrent
|
||||
|
||||
def test_mock_suggestion_algorithm_with_concurrency(self):
|
||||
mock_alg = _MockSuggestionAlgorithm(max_concurrent=5)
|
||||
assert _get_max_pending_trials(mock_alg) == 5
|
||||
|
||||
def test_mock_suggestion_algorithm_without_concurrency(self):
|
||||
mock_alg = _MockSuggestionAlgorithm()
|
||||
assert _get_max_pending_trials(mock_alg) == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,147 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||
|
||||
import ray
|
||||
from ray.air.execution import FixedResourceManager
|
||||
from ray.air.execution._internal import RayActorManager
|
||||
from ray.air.execution._internal.tracked_actor import TrackedActor
|
||||
from ray.air.execution.resources import ResourceManager, ResourceRequest
|
||||
from ray.train.tests.util import mock_storage_context
|
||||
from ray.tune.execution.tune_controller import TuneController
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.utils.resource_updater import _ResourceUpdater
|
||||
|
||||
|
||||
class NoopClassCache:
|
||||
def get(self, trainable_name: str):
|
||||
return trainable_name
|
||||
|
||||
|
||||
class BudgetResourceManager(FixedResourceManager):
|
||||
def __init__(self, total_resources: Dict[str, float]):
|
||||
self._allow_strict_pack = True
|
||||
self._total_resources = total_resources
|
||||
self._requested_resources = []
|
||||
self._used_resources = []
|
||||
|
||||
|
||||
class NoopActorManager(RayActorManager):
|
||||
def __init__(self, resource_manager: ResourceManager):
|
||||
super().__init__(resource_manager=resource_manager)
|
||||
|
||||
self.added_actors = []
|
||||
self.removed_actors = []
|
||||
self.scheduled_futures = []
|
||||
|
||||
def add_actor(
|
||||
self,
|
||||
cls: Union[Type, ray.actor.ActorClass],
|
||||
kwargs: Dict[str, Any],
|
||||
resource_request: ResourceRequest,
|
||||
*,
|
||||
on_start: Optional[Callable[[TrackedActor], None]] = None,
|
||||
on_stop: Optional[Callable[[TrackedActor], None]] = None,
|
||||
on_error: Optional[Callable[[TrackedActor, Exception], None]] = None,
|
||||
) -> TrackedActor:
|
||||
fake_actor_ref = uuid.uuid4().int
|
||||
tracked_actor = TrackedActor(
|
||||
fake_actor_ref, on_start=on_start, on_stop=on_stop, on_error=on_error
|
||||
)
|
||||
self._live_actors_to_ray_actors_resources[tracked_actor] = (fake_actor_ref,)
|
||||
self.added_actors.append((tracked_actor, cls, kwargs))
|
||||
return tracked_actor
|
||||
|
||||
def remove_actor(
|
||||
self,
|
||||
tracked_actor: TrackedActor,
|
||||
kill: bool = False,
|
||||
stop_future: Optional[ray.ObjectRef] = None,
|
||||
) -> None:
|
||||
self.removed_actors.append(tracked_actor)
|
||||
|
||||
def schedule_actor_task(
|
||||
self,
|
||||
tracked_actor: TrackedActor,
|
||||
method_name: str,
|
||||
args: Optional[Tuple] = None,
|
||||
kwargs: Optional[Dict] = None,
|
||||
on_result: Optional[Callable[[TrackedActor, Any], None]] = None,
|
||||
on_error: Optional[Callable[[TrackedActor, Exception], None]] = None,
|
||||
_return_future: bool = False,
|
||||
) -> Optional[int]:
|
||||
fake_ref = uuid.uuid4().int
|
||||
self.scheduled_futures.append(
|
||||
(fake_ref, tracked_actor, method_name, args, kwargs, on_result, on_error)
|
||||
)
|
||||
return fake_ref
|
||||
|
||||
@property
|
||||
def num_actor_tasks(self):
|
||||
return len(self.scheduled_futures)
|
||||
|
||||
def get_live_actors_resources(self):
|
||||
return {}
|
||||
|
||||
def next(self, timeout: Optional[Union[int, float]] = None) -> None:
|
||||
pass
|
||||
|
||||
def set_num_pending(self, num_pending: int):
|
||||
self._pending_actors_to_attrs = {i: None for i in range(num_pending)}
|
||||
|
||||
|
||||
class _FakeResourceUpdater(_ResourceUpdater):
|
||||
def __init__(self, resource_manager: BudgetResourceManager):
|
||||
self._resource_manager = resource_manager
|
||||
|
||||
def get_num_cpus(self):
|
||||
return self._resource_manager._total_resources.get("CPU", 0)
|
||||
|
||||
def get_num_gpus(self) -> int:
|
||||
return self._resource_manager._total_resources.get("GPU", 0)
|
||||
|
||||
def update_avail_resources(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestingTrial(Trial):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("storage", mock_storage_context())
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_trainable_cls(self):
|
||||
return self.trainable_name
|
||||
|
||||
def create_placement_group_factory(self):
|
||||
self.placement_group_factory = self._default_placement_group_factory
|
||||
|
||||
def set_ray_actor(self, ray_actor):
|
||||
pass
|
||||
|
||||
|
||||
def create_execution_test_objects(
|
||||
max_pending_trials: int = 8,
|
||||
resources: Optional[Dict[str, float]] = None,
|
||||
reuse_actors: bool = True,
|
||||
tune_controller_cls: Type[TuneController] = TuneController,
|
||||
**kwargs,
|
||||
):
|
||||
os.environ["TUNE_MAX_PENDING_TRIALS_PG"] = str(max_pending_trials)
|
||||
|
||||
resources = resources or {"CPU": 4}
|
||||
|
||||
storage = kwargs.pop("storage", mock_storage_context())
|
||||
|
||||
tune_controller = tune_controller_cls(
|
||||
reuse_actors=reuse_actors,
|
||||
storage=storage,
|
||||
**kwargs,
|
||||
)
|
||||
resource_manager = BudgetResourceManager(total_resources=resources)
|
||||
resource_updater = _FakeResourceUpdater(resource_manager)
|
||||
actor_manger = NoopActorManager(resource_manager)
|
||||
tune_controller._actor_manager = actor_manger
|
||||
tune_controller._class_cache = NoopClassCache()
|
||||
tune_controller._resource_updater = resource_updater
|
||||
|
||||
return tune_controller, actor_manger, resource_manager
|
||||
Reference in New Issue
Block a user