208 lines
7.2 KiB
Python
208 lines
7.2 KiB
Python
import os
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pyarrow.fs
|
|
import pytest
|
|
|
|
import ray
|
|
import ray.cloudpickle as ray_pickle
|
|
from ray.train import Checkpoint, SyncConfig
|
|
from ray.train._internal.storage import (
|
|
_VALIDATE_STORAGE_MARKER_FILENAME,
|
|
StorageContext,
|
|
_list_at_fs_path,
|
|
)
|
|
from ray.train.tests.test_new_persistence import _resolve_storage_type
|
|
|
|
|
|
@pytest.fixture(params=["nfs", "cloud", "custom_fs"])
|
|
def storage(request, tmp_path) -> StorageContext:
|
|
storage_type = request.param
|
|
with _resolve_storage_type(storage_type, tmp_path) as (
|
|
storage_path,
|
|
storage_filesystem,
|
|
):
|
|
yield StorageContext(
|
|
storage_path=storage_path,
|
|
experiment_dir_name=f"storage_type={storage_type}-{uuid.uuid4().hex}",
|
|
storage_filesystem=storage_filesystem,
|
|
trial_dir_name="trial_name",
|
|
sync_config=SyncConfig(
|
|
sync_artifacts=True, sync_artifacts_on_checkpoint=True, sync_period=1000
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="module")
|
|
def ray_init():
|
|
# NOTE: This is needed to set the `/tmp/ray/session_*` directory.
|
|
ray.init()
|
|
yield
|
|
ray.shutdown()
|
|
|
|
|
|
def test_custom_fs_validation(tmp_path):
|
|
"""Tests that invalid storage_path inputs give reasonable errors when a
|
|
custom filesystem is used."""
|
|
exp_name = "test"
|
|
StorageContext(
|
|
storage_path=str(tmp_path),
|
|
experiment_dir_name=exp_name,
|
|
storage_filesystem=pyarrow.fs.LocalFileSystem(),
|
|
)
|
|
|
|
mock_fs, _ = pyarrow.fs.FileSystem.from_uri("mock://a")
|
|
with pytest.raises(pyarrow.lib.ArrowInvalid) as excinfo:
|
|
StorageContext(
|
|
storage_path="mock:///a",
|
|
experiment_dir_name=exp_name,
|
|
storage_filesystem=mock_fs,
|
|
)
|
|
print("Custom fs with URI storage path error: ", excinfo.value)
|
|
|
|
StorageContext(
|
|
storage_path="a",
|
|
experiment_dir_name=exp_name,
|
|
storage_filesystem=mock_fs,
|
|
)
|
|
|
|
|
|
def test_storage_path_inputs():
|
|
"""Tests storage path input edge cases."""
|
|
exp_name = "test_storage_path"
|
|
|
|
# Relative paths don't work
|
|
with pytest.raises(pyarrow.lib.ArrowInvalid) as excinfo:
|
|
StorageContext(storage_path="./results", experiment_dir_name=exp_name)
|
|
assert "URI has empty scheme" in str(excinfo.value)
|
|
|
|
with pytest.raises(pyarrow.lib.ArrowInvalid) as excinfo:
|
|
StorageContext(storage_path="results", experiment_dir_name=exp_name)
|
|
assert "URI has empty scheme" in str(excinfo.value)
|
|
|
|
# Tilde paths sometimes raise... They do not work on the CI machines.
|
|
try:
|
|
StorageContext(storage_path="~/ray_results", experiment_dir_name=exp_name)
|
|
except pyarrow.lib.ArrowInvalid as e:
|
|
print(e)
|
|
pass
|
|
|
|
# Paths with lots of extra . .. and /
|
|
path = os.path.expanduser("~/ray_results")
|
|
path = os.path.join(path, ".", "..", "ray_results", ".")
|
|
path = path.replace(os.path.sep, os.path.sep * 2)
|
|
storage = StorageContext(storage_path=path, experiment_dir_name=exp_name)
|
|
|
|
storage.storage_filesystem.create_dir(
|
|
os.path.join(storage.storage_fs_path, "test_dir")
|
|
)
|
|
assert Path("~/ray_results/test_dir").expanduser().exists()
|
|
|
|
# Path objects work
|
|
StorageContext(storage_path=Path(path), experiment_dir_name=exp_name)
|
|
|
|
|
|
def test_storage_validation_marker(storage: StorageContext):
|
|
# A marker should have been created at initialization
|
|
storage._check_validation_file()
|
|
|
|
# Remove the marker to simulate being on a new node w/o access to the shared storage
|
|
storage.storage_filesystem.delete_file(
|
|
os.path.join(storage.experiment_fs_path, _VALIDATE_STORAGE_MARKER_FILENAME)
|
|
)
|
|
|
|
# Simulate passing the storage context around through the object store
|
|
# The constructor is NOT called again -- so the marker should not be checked here
|
|
# and we shouldn't raise an error
|
|
storage = ray_pickle.loads(ray_pickle.dumps(storage))
|
|
|
|
# We should raise an error when we try to checkpoint now.
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
storage.persist_current_checkpoint(Checkpoint.from_directory("/tmp/dummy"))
|
|
assert "Unable to set up cluster storage" in str(excinfo.value)
|
|
|
|
|
|
def test_persist_current_checkpoint(storage: StorageContext, tmp_path):
|
|
# Uploading a non-existent checkpoint directory should fail
|
|
with pytest.raises(FileNotFoundError):
|
|
storage.persist_current_checkpoint(
|
|
Checkpoint.from_directory("/tmp/nonexistent/checkpoint")
|
|
)
|
|
|
|
# Uploading an empty checkpoint directory
|
|
(tmp_path / "empty").mkdir()
|
|
storage.persist_current_checkpoint(Checkpoint.from_directory(tmp_path / "empty"))
|
|
assert (
|
|
_list_at_fs_path(storage.storage_filesystem, storage.checkpoint_fs_path) == []
|
|
)
|
|
|
|
# Normal use case: Uploading an checkpoint directory with files
|
|
(tmp_path / "regular").mkdir()
|
|
(tmp_path / "regular" / "1.txt").touch()
|
|
storage.persist_current_checkpoint(Checkpoint.from_directory(tmp_path / "regular"))
|
|
assert _list_at_fs_path(storage.storage_filesystem, storage.checkpoint_fs_path) == [
|
|
"1.txt"
|
|
]
|
|
|
|
storage.current_checkpoint_index += 1
|
|
|
|
# Persisting a checkpoint that is already at the correct path (for local fs case)
|
|
if isinstance(storage.storage_filesystem, pyarrow.fs.LocalFileSystem):
|
|
final_checkpoint_dir = Path(storage.checkpoint_fs_path)
|
|
final_checkpoint_dir.mkdir(parents=True)
|
|
(final_checkpoint_dir / "2.txt").touch()
|
|
storage.persist_current_checkpoint(
|
|
Checkpoint.from_directory(final_checkpoint_dir)
|
|
)
|
|
|
|
|
|
def test_persist_artifacts(storage: StorageContext):
|
|
"""Tests typical `StorageContext.persist_artifacts(force=True/False)` usage."""
|
|
trial_working_dir = Path(storage.trial_working_directory)
|
|
trial_working_dir.mkdir(parents=True)
|
|
trial_working_dir.joinpath("1.txt").touch()
|
|
|
|
storage.persist_artifacts()
|
|
storage.syncer.wait()
|
|
|
|
assert sorted(
|
|
_list_at_fs_path(storage.storage_filesystem, storage.trial_fs_path)
|
|
) == ["1.txt"]
|
|
|
|
trial_working_dir.joinpath("2.txt").touch()
|
|
|
|
# A new sync should not be triggered because sync_period is 1000 seconds
|
|
storage.persist_artifacts()
|
|
storage.syncer.wait()
|
|
|
|
# -> No change in the persisted files
|
|
assert sorted(
|
|
_list_at_fs_path(storage.storage_filesystem, storage.trial_fs_path)
|
|
) == ["1.txt"]
|
|
|
|
# This is what happens on `train.report` when a checkpoint is reported
|
|
# and `sync_artifacts_on_checkpoint=True`
|
|
storage.persist_artifacts(force=storage.sync_config.sync_artifacts_on_checkpoint)
|
|
assert sorted(
|
|
_list_at_fs_path(storage.storage_filesystem, storage.trial_fs_path)
|
|
) == ["1.txt", "2.txt"]
|
|
|
|
|
|
def test_persist_artifacts_failures(storage: StorageContext):
|
|
"""Tests `StorageContext.persist_artifacts` edge cases (empty directory)."""
|
|
# Uploading before the trial directory has been created should fail
|
|
with pytest.raises(FileNotFoundError):
|
|
storage.persist_artifacts()
|
|
if storage.syncer:
|
|
storage.syncer.wait()
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
storage.persist_artifacts(force=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|