157 lines
4.9 KiB
Python
157 lines
4.9 KiB
Python
import logging
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from ray.exceptions import RayActorError, RayTaskError
|
|
from ray.tests.conftest import propagate_logs # noqa
|
|
from ray.train._internal.session import _TrainingResult
|
|
from ray.train._internal.storage import StorageContext
|
|
from ray.train.constants import RAY_TRAIN_COUNT_PREEMPTION_AS_FAILURE
|
|
from ray.train.tests.util import mock_storage_context
|
|
from ray.tune import Checkpoint
|
|
from ray.tune.experiment import Trial
|
|
|
|
|
|
@pytest.fixture
|
|
def trial(tmp_path):
|
|
yield Trial(
|
|
"mock",
|
|
stub=True,
|
|
storage=mock_storage_context(storage_path=str(tmp_path)),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("count_preemption_errors", [False, True])
|
|
def test_handle_preemption_error(
|
|
trial: Trial, count_preemption_errors: bool, monkeypatch
|
|
):
|
|
"""Check that the Trial counts preemption errors correctly."""
|
|
if count_preemption_errors:
|
|
monkeypatch.setenv(RAY_TRAIN_COUNT_PREEMPTION_AS_FAILURE, "1")
|
|
|
|
# Case 1: Directly raised (preemption) RayActorError
|
|
class PreemptionRayActorError(RayActorError):
|
|
def preempted(self) -> bool:
|
|
return True
|
|
|
|
err = PreemptionRayActorError()
|
|
trial.handle_error(err)
|
|
assert trial.num_failures == (1 if count_preemption_errors else 0)
|
|
|
|
# Case 2: RayTaskError, where the cause is a (preemption) RayActorError
|
|
wrapped_err = RayTaskError(
|
|
function_name="test", traceback_str="traceback_str", cause=err
|
|
)
|
|
trial.handle_error(wrapped_err)
|
|
assert trial.num_failures == (2 if count_preemption_errors else 0)
|
|
|
|
# Case 3: Non-preemption error
|
|
non_preempted_err = RayActorError()
|
|
trial.handle_error(non_preempted_err)
|
|
assert trial.num_failures == (3 if count_preemption_errors else 1)
|
|
|
|
|
|
def test_load_trial_from_json_state():
|
|
"""Check that serializing a trial to a JSON string with `Trial.get_json_state`
|
|
and then creating a new trial using the `Trial.from_json_state` alternate
|
|
constructor loads the trial with equivalent state."""
|
|
trial = Trial(
|
|
"MockTrainable",
|
|
stub=True,
|
|
trial_id="abcd1234",
|
|
storage=mock_storage_context(),
|
|
)
|
|
trial.create_placement_group_factory()
|
|
trial.init_local_path()
|
|
trial.status = Trial.TERMINATED
|
|
|
|
# After loading, the trial state should be the same
|
|
json_state, _ = trial.get_json_state()
|
|
new_trial = Trial.from_json_state(json_state, stub=True)
|
|
assert new_trial.get_json_state()[0] == json_state
|
|
|
|
|
|
def test_set_storage(tmp_path):
|
|
"""Test that setting the trial's storage context will update the tracked
|
|
checkpoint paths."""
|
|
original_storage = mock_storage_context()
|
|
trial = Trial(
|
|
"MockTrainable",
|
|
stub=True,
|
|
trial_id="abcd1234",
|
|
storage=original_storage,
|
|
)
|
|
|
|
result_1 = _TrainingResult(
|
|
checkpoint=Checkpoint.from_directory(original_storage.checkpoint_fs_path),
|
|
metrics={},
|
|
)
|
|
trial.on_checkpoint(result_1)
|
|
|
|
result_2 = _TrainingResult(
|
|
checkpoint=Checkpoint.from_directory(original_storage.checkpoint_fs_path),
|
|
metrics={},
|
|
)
|
|
trial.on_checkpoint(result_2)
|
|
|
|
new_storage = StorageContext(
|
|
storage_path=tmp_path / "new_storage_path",
|
|
experiment_dir_name="new_name",
|
|
trial_dir_name="new_trial",
|
|
)
|
|
trial.set_storage(new_storage)
|
|
|
|
assert result_1.checkpoint.path.startswith(new_storage.trial_fs_path)
|
|
assert result_2.checkpoint.path.startswith(new_storage.trial_fs_path)
|
|
|
|
|
|
def test_trial_logdir_length():
|
|
"""Test that trial local paths with a long logdir are truncated"""
|
|
trial = Trial(
|
|
trainable_name="none",
|
|
stub=True,
|
|
config={"a" * 50: 5.0 / 7, "b" * 50: "long" * 40},
|
|
storage=mock_storage_context(),
|
|
)
|
|
trial.init_local_path()
|
|
assert len(trial.storage.trial_dir_name) < 200
|
|
|
|
|
|
def test_should_stop(caplog, propagate_logs): # noqa
|
|
"""Test whether `Trial.should_stop()` works as expected given a result dict."""
|
|
trial = Trial(
|
|
"MockTrainable",
|
|
stub=True,
|
|
trial_id="abcd1234",
|
|
stopping_criterion={"a": 10.0, "b/c": 20.0},
|
|
)
|
|
|
|
# Criterion is not reached yet -> don't stop.
|
|
result = {"a": 9.999, "b/c": 0.0, "some_other_key": True}
|
|
assert not trial.should_stop(result)
|
|
|
|
# Criterion is exactly reached -> stop.
|
|
result = {"a": 10.0, "b/c": 0.0, "some_other_key": False}
|
|
assert trial.should_stop(result)
|
|
|
|
# Criterion is exceeded -> stop.
|
|
result = {"a": 10000.0, "b/c": 0.0, "some_other_key": False}
|
|
assert trial.should_stop(result)
|
|
|
|
# Test nested criterion.
|
|
result = {"a": 5.0, "b/c": 1000.0, "some_other_key": False}
|
|
assert trial.should_stop(result)
|
|
|
|
# Test criterion NOT found in result metrics.
|
|
result = {"b/c": 1000.0}
|
|
with caplog.at_level(logging.WARNING):
|
|
trial.should_stop(result)
|
|
assert (
|
|
"Stopping criterion 'a' not found in result dict! Available keys are ['b/c']."
|
|
) in caplog.text
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", __file__]))
|