360 lines
13 KiB
Python
360 lines
13 KiB
Python
import uuid
|
|
from typing import Optional
|
|
from unittest.mock import create_autospec
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray.train import CheckpointConfig
|
|
from ray.train._internal.session import _TrainingResult
|
|
from ray.train.v2._internal.exceptions import CheckpointManagerInitializationError
|
|
from ray.train.v2._internal.execution.checkpoint.checkpoint_manager import (
|
|
CheckpointManager,
|
|
)
|
|
from ray.train.v2._internal.execution.storage import StorageContext
|
|
from ray.train.v2._internal.execution.worker_group import Worker
|
|
from ray.train.v2.api.reported_checkpoint import ReportedCheckpointStatus
|
|
from ray.train.v2.tests.util import create_dummy_training_reports
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="module")
|
|
def ray_start_4_cpus():
|
|
ray.init(num_cpus=4)
|
|
yield
|
|
ray.shutdown()
|
|
|
|
|
|
def _checkpoint_managers_equal(cm1: CheckpointManager, cm2: CheckpointManager) -> bool:
|
|
"""
|
|
Compare two checkpoint managers for equality.
|
|
Ignore uuid differences of all the checkpoints recorded.
|
|
"""
|
|
|
|
def _training_results_equal(
|
|
tr1: Optional[_TrainingResult], tr2: Optional[_TrainingResult]
|
|
) -> bool:
|
|
if not tr1 and not tr2:
|
|
return True
|
|
if not tr1 or not tr2:
|
|
return False
|
|
return (
|
|
tr1.metrics == tr2.metrics
|
|
and tr1.checkpoint.path == tr2.checkpoint.path
|
|
and tr1.checkpoint.filesystem == tr2.checkpoint.filesystem
|
|
)
|
|
|
|
def _checkpoint_to_report_indices_equal(
|
|
cm1: CheckpointManager, cm2: CheckpointManager
|
|
) -> bool:
|
|
# Do this because Checkpoint and Filesystem are not hashable.
|
|
cm1_dict = {
|
|
checkpoint.path: report_index
|
|
for checkpoint, report_index in cm1._checkpoint_to_report_index.items()
|
|
}
|
|
cm2_dict = {
|
|
checkpoint.path: report_index
|
|
for checkpoint, report_index in cm2._checkpoint_to_report_index.items()
|
|
}
|
|
return cm1_dict == cm2_dict
|
|
|
|
if cm1._checkpoint_config != cm2._checkpoint_config:
|
|
return False
|
|
if not _training_results_equal(
|
|
cm1.latest_checkpoint_result, cm2.latest_checkpoint_result
|
|
):
|
|
return False
|
|
if not _training_results_equal(
|
|
cm1.best_checkpoint_result, cm2.best_checkpoint_result
|
|
):
|
|
return False
|
|
if len(cm1.best_checkpoint_results) != len(cm2.best_checkpoint_results):
|
|
return False
|
|
for tr1, tr2 in zip(cm1.best_checkpoint_results, cm2.best_checkpoint_results):
|
|
if not _training_results_equal(tr1, tr2):
|
|
return False
|
|
if cm1._current_report_index != cm2._current_report_index:
|
|
return False
|
|
if not _checkpoint_to_report_indices_equal(cm1, cm2):
|
|
return False
|
|
return True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"checkpoint_config",
|
|
[
|
|
CheckpointConfig(),
|
|
CheckpointConfig(
|
|
num_to_keep=1,
|
|
checkpoint_score_attribute="score",
|
|
checkpoint_score_order="max",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_save_load_state_equivalence(
|
|
monkeypatch, tmp_path, checkpoint_config: CheckpointConfig
|
|
):
|
|
# Use async here because register_checkpoint creates an async task
|
|
|
|
# Mock the delete function as we don't want report checkpoints to be deleted.
|
|
monkeypatch.setattr(
|
|
ray.train.v2._internal.execution.checkpoint.checkpoint_manager,
|
|
"delete_fs_path",
|
|
lambda *args, **kwargs: None,
|
|
)
|
|
exp_name = f"checkpoint_manager_test-{uuid.uuid4().hex}"
|
|
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name=exp_name,
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=checkpoint_config,
|
|
)
|
|
training_reports = create_dummy_training_reports(
|
|
num_results=2, storage_context=storage_context
|
|
) + create_dummy_training_reports(
|
|
num_results=1,
|
|
storage_context=storage_context,
|
|
include_validation=True,
|
|
starting_checkpoint_index=2,
|
|
)
|
|
|
|
# Register the training results into checkpoint manager
|
|
for i, tr in enumerate(training_reports):
|
|
checkpoint_manager.register_checkpoint(tr)
|
|
assert checkpoint_manager._current_report_index == i + 1
|
|
loaded_checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=checkpoint_config,
|
|
)
|
|
assert _checkpoint_managers_equal(checkpoint_manager, loaded_checkpoint_manager)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"json_state,match",
|
|
[
|
|
(
|
|
'{"dummy": "1", "dummy_dict": {"key": "value"}}',
|
|
"You are loading a checkpoint manager snapshot saved with an unknown Ray version but",
|
|
),
|
|
('{"ray_version": "2.0.0", "dummy": "1", "dummy_dict": {"key": "value"', None),
|
|
(
|
|
'{"ray_version": "2.0.0", "dummy": "1", "dummy_dict": {"key": "value"}}',
|
|
"You are loading a checkpoint manager snapshot saved with Ray version 2.0.0 but",
|
|
),
|
|
],
|
|
)
|
|
def test_load_state_error(tmp_path, json_state, match):
|
|
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="load_state_error_experiment",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=CheckpointConfig(),
|
|
)
|
|
with pytest.raises(
|
|
CheckpointManagerInitializationError,
|
|
match=match,
|
|
):
|
|
checkpoint_manager._load_state(json_state)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_before_init_train_context(tmp_path):
|
|
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="my_experiment_name",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=CheckpointConfig(),
|
|
)
|
|
workers = [create_autospec(Worker, instance=True) for _ in range(4)]
|
|
|
|
# Assert without a checkpoint.
|
|
assert checkpoint_manager.before_init_train_context(workers) == {
|
|
"checkpoint": [None] * 4,
|
|
"current_report_index": [0] * 4,
|
|
}
|
|
|
|
# Assert with a checkpoint
|
|
latest_checkpoint_report = create_dummy_training_reports(1, storage_context)[0]
|
|
checkpoint_manager.register_checkpoint(latest_checkpoint_report)
|
|
assert checkpoint_manager.before_init_train_context(workers) == {
|
|
"checkpoint": [latest_checkpoint_report.checkpoint] * 4,
|
|
"current_report_index": [1] * 4,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_checkpoint_management(tmp_path):
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="pending_checkpoint_management_experiment",
|
|
)
|
|
checkpoint_config = CheckpointConfig(
|
|
num_to_keep=1,
|
|
checkpoint_score_attribute="score",
|
|
checkpoint_score_order="max",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=checkpoint_config,
|
|
)
|
|
(
|
|
low_initial_high_final_training_report,
|
|
high_initial_low_final_training_report,
|
|
final_training_report,
|
|
) = create_dummy_training_reports(
|
|
num_results=3, storage_context=storage_context, include_validation=True
|
|
)
|
|
final_training_report.validation = False
|
|
scoreless_training_report = create_dummy_training_reports(
|
|
num_results=1,
|
|
storage_context=storage_context,
|
|
include_metrics=False,
|
|
starting_checkpoint_index=3,
|
|
)[0]
|
|
|
|
# Register pending/final/unknown checkpoints and verify their storage
|
|
checkpoint_manager.register_checkpoint(low_initial_high_final_training_report)
|
|
checkpoint_manager.register_checkpoint(final_training_report)
|
|
checkpoint_manager.register_checkpoint(scoreless_training_report)
|
|
checkpoint_manager.register_checkpoint(high_initial_low_final_training_report)
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
low_initial_high_final_training_report.checkpoint, # keep pending
|
|
high_initial_low_final_training_report.checkpoint, # keep pending/latest
|
|
final_training_report.checkpoint, # keep highest final score so far
|
|
]
|
|
|
|
# Assert checkpoint state after all tasks are done
|
|
checkpoint_manager.update_checkpoints_with_validation_result(
|
|
{
|
|
low_initial_high_final_training_report.checkpoint: (
|
|
{"score": 200},
|
|
ReportedCheckpointStatus.VALIDATED,
|
|
),
|
|
high_initial_low_final_training_report.checkpoint: (
|
|
{"score": 100},
|
|
ReportedCheckpointStatus.VALIDATED,
|
|
),
|
|
}
|
|
)
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
high_initial_low_final_training_report.checkpoint, # keep latest checkpoint
|
|
low_initial_high_final_training_report.checkpoint, # keep highest score checkpoint
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_checkpoint_management_break_ties_by_report_index(tmp_path):
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="pending_checkpoint_management_break_ties_by_report_index_experiment",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=CheckpointConfig(),
|
|
)
|
|
training_reports = create_dummy_training_reports(
|
|
num_results=2, storage_context=storage_context, include_validation=True
|
|
)
|
|
checkpoint_manager.register_checkpoint(training_reports[0])
|
|
checkpoint_manager.register_checkpoint(training_reports[1])
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
training_reports[0].checkpoint,
|
|
training_reports[1].checkpoint,
|
|
]
|
|
checkpoint_manager.update_checkpoints_with_validation_result(
|
|
{
|
|
training_reports[1].checkpoint: ({}, ReportedCheckpointStatus.VALIDATED),
|
|
}
|
|
)
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
training_reports[0].checkpoint,
|
|
training_reports[1].checkpoint,
|
|
]
|
|
checkpoint_manager.update_checkpoints_with_validation_result(
|
|
{
|
|
training_reports[0].checkpoint: ({}, ReportedCheckpointStatus.VALIDATED),
|
|
}
|
|
)
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
training_reports[0].checkpoint,
|
|
training_reports[1].checkpoint,
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_checkpoint_management_finalized_checkpoint(tmp_path):
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="pending_checkpoint_management_experiment",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=CheckpointConfig(
|
|
checkpoint_score_attribute="score",
|
|
checkpoint_score_order="max",
|
|
),
|
|
)
|
|
training_reports = create_dummy_training_reports(
|
|
num_results=2, storage_context=storage_context
|
|
)
|
|
checkpoint_manager.register_checkpoint(training_reports[0])
|
|
checkpoint_manager.register_checkpoint(training_reports[1])
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
training_reports[0].checkpoint,
|
|
training_reports[1].checkpoint,
|
|
]
|
|
checkpoint_manager.update_checkpoints_with_validation_result(
|
|
{
|
|
training_reports[0].checkpoint: (
|
|
{"score": 100},
|
|
ReportedCheckpointStatus.VALIDATED,
|
|
),
|
|
}
|
|
)
|
|
assert [tr.checkpoint for tr in checkpoint_manager._checkpoint_results] == [
|
|
training_reports[0].checkpoint,
|
|
training_reports[1].checkpoint,
|
|
]
|
|
|
|
|
|
def test_update_checkpoints_with_metrics_not_in_checkpoint_results(tmp_path):
|
|
storage_context = StorageContext(
|
|
storage_path=tmp_path,
|
|
experiment_dir_name="update_checkpoints_with_metrics_error_experiment",
|
|
)
|
|
checkpoint_manager = CheckpointManager(
|
|
storage_context=storage_context,
|
|
checkpoint_config=CheckpointConfig(),
|
|
)
|
|
training_reports = create_dummy_training_reports(
|
|
num_results=1, storage_context=storage_context
|
|
)
|
|
checkpoint_manager._pending_training_results[training_reports[0].checkpoint] = (
|
|
_TrainingResult(training_reports[0].checkpoint, training_reports[0].metrics),
|
|
training_reports[0].validation,
|
|
)
|
|
with pytest.raises(ValueError):
|
|
checkpoint_manager.update_checkpoints_with_validation_result(
|
|
{
|
|
training_reports[0].checkpoint: (
|
|
{"score": 100},
|
|
ReportedCheckpointStatus.VALIDATED,
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|