281 lines
9.8 KiB
Python
281 lines
9.8 KiB
Python
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from collections import OrderedDict
|
|
from unittest.mock import patch
|
|
|
|
import ray
|
|
from ray import tune
|
|
from ray.air._internal.checkpoint_manager import CheckpointStorage, _TrackedCheckpoint
|
|
from ray.air.constants import TRAINING_ITERATION
|
|
from ray.rllib import _register_all
|
|
from ray.tune import Callback
|
|
from ray.tune.callback import warnings
|
|
from ray.tune.execution.ray_trial_executor import (
|
|
RayTrialExecutor,
|
|
_ExecutorEvent,
|
|
_ExecutorEventType,
|
|
)
|
|
from ray.tune.execution.trial_runner import TrialRunner
|
|
from ray.tune.experiment import Experiment, Trial
|
|
|
|
|
|
class TestCallback(Callback):
|
|
def __init__(self):
|
|
self.state = OrderedDict()
|
|
|
|
def setup(self, **info):
|
|
self.state["setup"] = info
|
|
|
|
def on_step_begin(self, **info):
|
|
self.state["step_begin"] = info
|
|
|
|
def on_step_end(self, **info):
|
|
self.state["step_end"] = info
|
|
|
|
def on_trial_start(self, **info):
|
|
self.state["trial_start"] = info
|
|
|
|
def on_trial_restore(self, **info):
|
|
self.state["trial_restore"] = info
|
|
|
|
def on_trial_save(self, **info):
|
|
self.state["trial_save"] = info
|
|
|
|
def on_trial_result(self, **info):
|
|
self.state["trial_result"] = info
|
|
result = info["result"]
|
|
trial = info["trial"]
|
|
assert result.get(TRAINING_ITERATION, None) != trial.last_result.get(
|
|
TRAINING_ITERATION, None
|
|
)
|
|
|
|
def on_trial_complete(self, **info):
|
|
self.state["trial_complete"] = info
|
|
|
|
def on_trial_error(self, **info):
|
|
self.state["trial_fail"] = info
|
|
|
|
def on_experiment_end(self, **info):
|
|
self.state["experiment_end"] = info
|
|
|
|
|
|
# TODO(xwjiang): Move this to a testing util.
|
|
class _MockTrialExecutor(RayTrialExecutor):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.next_future_result = None
|
|
|
|
def start_trial(self, trial: Trial):
|
|
trial.status = Trial.RUNNING
|
|
return True
|
|
|
|
def continue_training(self, trial: Trial):
|
|
pass
|
|
|
|
def get_next_executor_event(self, live_trials, next_trial_exists):
|
|
return self.next_future_result
|
|
|
|
|
|
class TrialRunnerCallbacks(unittest.TestCase):
|
|
def setUp(self):
|
|
ray.init()
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.callback = TestCallback()
|
|
self.executor = _MockTrialExecutor()
|
|
self.trial_runner = TrialRunner(
|
|
trial_executor=self.executor, callbacks=[self.callback]
|
|
)
|
|
# experiment would never be None normally, but it's fine for testing
|
|
self.trial_runner.setup_experiments(experiments=[None], total_num_samples=1)
|
|
|
|
def tearDown(self):
|
|
ray.shutdown()
|
|
_register_all() # re-register the evicted objects
|
|
if "CUDA_VISIBLE_DEVICES" in os.environ:
|
|
del os.environ["CUDA_VISIBLE_DEVICES"]
|
|
shutil.rmtree(self.tmpdir)
|
|
|
|
def testCallbackSteps(self):
|
|
trials = [Trial("__fake", trial_id="one"), Trial("__fake", trial_id="two")]
|
|
for t in trials:
|
|
self.trial_runner.add_trial(t)
|
|
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.PG_READY
|
|
)
|
|
self.trial_runner.step()
|
|
|
|
# Trial 1 has been started
|
|
self.assertEqual(self.callback.state["trial_start"]["iteration"], 0)
|
|
self.assertEqual(self.callback.state["trial_start"]["trial"].trial_id, "one")
|
|
|
|
# All these events haven't happened, yet
|
|
self.assertTrue(
|
|
all(
|
|
k not in self.callback.state
|
|
for k in [
|
|
"trial_restore",
|
|
"trial_save",
|
|
"trial_result",
|
|
"trial_complete",
|
|
"trial_fail",
|
|
"experiment_end",
|
|
]
|
|
)
|
|
)
|
|
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.PG_READY
|
|
)
|
|
self.trial_runner.step()
|
|
|
|
# Iteration not increased yet
|
|
self.assertEqual(self.callback.state["step_begin"]["iteration"], 1)
|
|
|
|
# Iteration increased
|
|
self.assertEqual(self.callback.state["step_end"]["iteration"], 2)
|
|
|
|
# Second trial has been just started
|
|
self.assertEqual(self.callback.state["trial_start"]["iteration"], 1)
|
|
self.assertEqual(self.callback.state["trial_start"]["trial"].trial_id, "two")
|
|
|
|
# Just a placeholder object ref for cp.value.
|
|
cp = _TrackedCheckpoint(
|
|
dir_or_data=ray.put(1),
|
|
storage_mode=CheckpointStorage.PERSISTENT,
|
|
metrics={TRAINING_ITERATION: 0},
|
|
)
|
|
trials[0].temporary_state.saving_to = cp
|
|
|
|
# Let the first trial save a checkpoint
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.SAVING_RESULT,
|
|
trial=trials[0],
|
|
result={_ExecutorEvent.KEY_FUTURE_RESULT: "__checkpoint"},
|
|
)
|
|
self.trial_runner.step()
|
|
self.assertEqual(self.callback.state["trial_save"]["iteration"], 2)
|
|
self.assertEqual(self.callback.state["trial_save"]["trial"].trial_id, "one")
|
|
|
|
# Let the second trial send a result
|
|
result = {TRAINING_ITERATION: 1, "metric": 800, "done": False}
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.TRAINING_RESULT,
|
|
trial=trials[1],
|
|
result={_ExecutorEvent.KEY_FUTURE_RESULT: result},
|
|
)
|
|
self.assertTrue(not trials[1].has_reported_at_least_once)
|
|
self.trial_runner.step()
|
|
self.assertEqual(self.callback.state["trial_result"]["iteration"], 3)
|
|
self.assertEqual(self.callback.state["trial_result"]["trial"].trial_id, "two")
|
|
self.assertEqual(self.callback.state["trial_result"]["result"]["metric"], 800)
|
|
self.assertEqual(trials[1].last_result["metric"], 800)
|
|
|
|
# Let the second trial restore from a checkpoint
|
|
trials[1].temporary_state.restoring_from = cp
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.RESTORING_RESULT,
|
|
trial=trials[1],
|
|
result={_ExecutorEvent.KEY_FUTURE_RESULT: None},
|
|
)
|
|
self.trial_runner.step()
|
|
self.assertEqual(self.callback.state["trial_restore"]["iteration"], 4)
|
|
self.assertEqual(self.callback.state["trial_restore"]["trial"].trial_id, "two")
|
|
|
|
# Let the second trial finish
|
|
trials[1].temporary_state.restoring_from = None
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.TRAINING_RESULT,
|
|
trial=trials[1],
|
|
result={
|
|
_ExecutorEvent.KEY_FUTURE_RESULT: {
|
|
TRAINING_ITERATION: 2,
|
|
"metric": 900,
|
|
"done": True,
|
|
}
|
|
},
|
|
)
|
|
self.trial_runner.step()
|
|
self.assertEqual(self.callback.state["trial_complete"]["iteration"], 5)
|
|
self.assertEqual(self.callback.state["trial_complete"]["trial"].trial_id, "two")
|
|
|
|
# Let the first trial error
|
|
self.executor.next_future_result = _ExecutorEvent(
|
|
event_type=_ExecutorEventType.TRAINING_RESULT,
|
|
trial=trials[0],
|
|
result={_ExecutorEvent.KEY_EXCEPTION: Exception()},
|
|
)
|
|
self.trial_runner.step()
|
|
self.assertEqual(self.callback.state["trial_fail"]["iteration"], 6)
|
|
self.assertEqual(self.callback.state["trial_fail"]["trial"].trial_id, "one")
|
|
|
|
def testCallbacksEndToEnd(self):
|
|
def train_fn(config):
|
|
if config["do"] == "save":
|
|
with tune.checkpoint_dir(0):
|
|
pass
|
|
tune.report(metric=1)
|
|
elif config["do"] == "fail":
|
|
raise RuntimeError("I am failing on purpose.")
|
|
elif config["do"] == "delay":
|
|
time.sleep(2)
|
|
tune.report(metric=20)
|
|
|
|
config = {"do": tune.grid_search(["save", "fail", "delay"])}
|
|
|
|
tune.run(
|
|
train_fn,
|
|
config=config,
|
|
raise_on_failed_trial=False,
|
|
callbacks=[self.callback],
|
|
)
|
|
|
|
self.assertIn("setup", self.callback.state)
|
|
self.assertTrue(self.callback.state["setup"] is not None)
|
|
keys = Experiment.PUBLIC_KEYS.copy()
|
|
keys.add("total_num_samples")
|
|
for key in keys:
|
|
self.assertIn(key, self.callback.state["setup"])
|
|
# check if it was added first
|
|
self.assertTrue(list(self.callback.state)[0] == "setup")
|
|
self.assertEqual(
|
|
self.callback.state["trial_fail"]["trial"].config["do"], "fail"
|
|
)
|
|
self.assertEqual(
|
|
self.callback.state["trial_save"]["trial"].config["do"], "save"
|
|
)
|
|
self.assertEqual(
|
|
self.callback.state["trial_result"]["trial"].config["do"], "delay"
|
|
)
|
|
self.assertEqual(
|
|
self.callback.state["trial_complete"]["trial"].config["do"], "delay"
|
|
)
|
|
self.assertIn("experiment_end", self.callback.state)
|
|
# check if it was added last
|
|
self.assertTrue(list(self.callback.state)[-1] == "experiment_end")
|
|
|
|
@patch.object(warnings, "warn")
|
|
def testCallbackSetupBackwardsCompatible(self, mocked_warning_method):
|
|
class NoExperimentInSetupCallback(Callback):
|
|
# Old method definition didn't take in **experiment.public_spec
|
|
def setup(self):
|
|
return
|
|
|
|
callback = NoExperimentInSetupCallback()
|
|
trial_runner = TrialRunner(callbacks=[callback])
|
|
trial_runner.setup_experiments(
|
|
experiments=[Experiment("", lambda x: x)], total_num_samples=1
|
|
)
|
|
mocked_warning_method.assert_called_once()
|
|
self.assertIn("Please update", mocked_warning_method.call_args_list[0][0][0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pytest
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|