155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
import pyarrow
|
|
import pytest
|
|
|
|
import ray
|
|
from ray import train
|
|
from ray.air._internal.uri_utils import URI
|
|
from ray.air.constants import EXPR_RESULT_FILE
|
|
from ray.train import CheckpointConfig, Result, RunConfig, ScalingConfig
|
|
from ray.train.base_trainer import TrainingFailedError
|
|
from ray.train.tests.util import create_dict_checkpoint, load_dict_checkpoint
|
|
from ray.train.torch import TorchTrainer
|
|
from ray.tune import TuneConfig, Tuner
|
|
|
|
_PARAM_SPACE = {"a": 1, "b": 2}
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_4_cpus():
|
|
address_info = ray.init(num_cpus=4)
|
|
yield address_info
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
def build_dummy_trainer(configs):
|
|
def worker_loop(_config):
|
|
for i in range(configs["NUM_ITERATIONS"]):
|
|
# Do some random reports in between checkpoints.
|
|
train.report({"metric_a": -100, "metric_b": -100})
|
|
|
|
if ray.train.get_context().get_world_rank() == 0:
|
|
with create_dict_checkpoint({"iter": i}) as checkpoint:
|
|
train.report(
|
|
metrics={"metric_a": i, "metric_b": -i},
|
|
checkpoint=checkpoint,
|
|
)
|
|
else:
|
|
train.report(metrics={"metric_a": i, "metric_b": -i})
|
|
raise RuntimeError()
|
|
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=worker_loop,
|
|
train_loop_config=_PARAM_SPACE,
|
|
scaling_config=ScalingConfig(num_workers=2, use_gpu=False),
|
|
run_config=RunConfig(
|
|
name=configs["EXP_NAME"],
|
|
storage_path=configs["STORAGE_PATH"],
|
|
checkpoint_config=CheckpointConfig(
|
|
num_to_keep=configs["NUM_CHECKPOINTS"],
|
|
checkpoint_score_attribute="metric_a",
|
|
checkpoint_score_order="max",
|
|
),
|
|
),
|
|
)
|
|
return trainer
|
|
|
|
|
|
def build_dummy_tuner(configs):
|
|
return Tuner(
|
|
build_dummy_trainer(configs),
|
|
param_space={"train_loop_config": _PARAM_SPACE},
|
|
tune_config=TuneConfig(num_samples=1),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("storage", ["local", "remote"])
|
|
@pytest.mark.parametrize("mode", ["trainer", "tuner"])
|
|
def test_result_restore(ray_start_4_cpus, tmpdir, mock_s3_bucket_uri, storage, mode):
|
|
NUM_ITERATIONS = 5
|
|
NUM_CHECKPOINTS = 3
|
|
if storage == "local":
|
|
storage_path = str(tmpdir)
|
|
elif storage == "remote":
|
|
storage_path = str(URI(mock_s3_bucket_uri))
|
|
|
|
exp_name = "test_result_restore"
|
|
|
|
configs = {
|
|
"EXP_NAME": exp_name,
|
|
"STORAGE_PATH": storage_path,
|
|
"NUM_ITERATIONS": NUM_ITERATIONS,
|
|
"NUM_CHECKPOINTS": NUM_CHECKPOINTS,
|
|
}
|
|
|
|
if mode == "trainer":
|
|
trainer = build_dummy_trainer(configs)
|
|
with pytest.raises(TrainingFailedError):
|
|
trainer.fit()
|
|
elif mode == "tuner":
|
|
tuner = build_dummy_tuner(configs)
|
|
tuner.fit()
|
|
|
|
# Find the trial directory to restore
|
|
exp_dir = str(URI(storage_path) / exp_name)
|
|
fs, fs_exp_dir = pyarrow.fs.FileSystem.from_uri(exp_dir)
|
|
for item in fs.get_file_info(pyarrow.fs.FileSelector(fs_exp_dir)):
|
|
if item.type == pyarrow.fs.FileType.Directory and item.base_name.startswith(
|
|
"TorchTrainer"
|
|
):
|
|
trial_dir = str(URI(exp_dir) / item.base_name)
|
|
break
|
|
|
|
# [1] Restore from path
|
|
result = Result.from_path(trial_dir)
|
|
|
|
# Check if we restored all checkpoints
|
|
assert result.checkpoint
|
|
assert len(result.best_checkpoints) == NUM_CHECKPOINTS
|
|
|
|
"""
|
|
Top-3 checkpoints with metrics:
|
|
|
|
| iter | metric_a metric_b
|
|
checkpoint_000004 4 4 -4
|
|
checkpoint_000003 3 3 -3
|
|
checkpoint_000002 2 2 -2
|
|
"""
|
|
# Check if the checkpoints bounded with correct metrics
|
|
best_ckpt_a = result.get_best_checkpoint(metric="metric_a", mode="max")
|
|
assert load_dict_checkpoint(best_ckpt_a)["iter"] == NUM_ITERATIONS - 1
|
|
|
|
best_ckpt_b = result.get_best_checkpoint(metric="metric_b", mode="max")
|
|
assert load_dict_checkpoint(best_ckpt_b)["iter"] == NUM_ITERATIONS - NUM_CHECKPOINTS
|
|
|
|
with pytest.raises(RuntimeError, match="Invalid metric name.*"):
|
|
result.get_best_checkpoint(metric="invalid_metric", mode="max")
|
|
|
|
# Check if we properly restored errors
|
|
assert isinstance(result.error, RuntimeError)
|
|
|
|
# Check that the config is properly formatted in the result metrics
|
|
assert result.metrics.get("config") == {"train_loop_config": _PARAM_SPACE}
|
|
|
|
# [2] Restore from path without result.json
|
|
fs.delete_file((URI(trial_dir) / EXPR_RESULT_FILE).path)
|
|
result = Result.from_path(trial_dir)
|
|
|
|
# Do the same checks as above
|
|
assert result.checkpoint
|
|
assert len(result.best_checkpoints) == NUM_CHECKPOINTS
|
|
|
|
best_ckpt_a = result.get_best_checkpoint(metric="metric_a", mode="max")
|
|
assert load_dict_checkpoint(best_ckpt_a)["iter"] == NUM_ITERATIONS - 1
|
|
|
|
best_ckpt_b = result.get_best_checkpoint(metric="metric_b", mode="max")
|
|
assert load_dict_checkpoint(best_ckpt_b)["iter"] == NUM_ITERATIONS - NUM_CHECKPOINTS
|
|
|
|
assert isinstance(result.error, RuntimeError)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|