211 lines
6.0 KiB
Python
211 lines
6.0 KiB
Python
"""Unit tests for AIR telemetry."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pyarrow.fs
|
|
import pytest
|
|
from packaging.version import Version
|
|
|
|
import ray
|
|
from ray import train, tune
|
|
from ray._common.usage.usage_lib import TagKey
|
|
from ray.air._internal import usage as air_usage
|
|
from ray.air._internal.usage import AirEntrypoint
|
|
from ray.air.integrations import comet, mlflow, wandb
|
|
from ray.train._internal.storage import StorageContext
|
|
from ray.tune.callback import Callback
|
|
from ray.tune.experiment.experiment import Experiment
|
|
from ray.tune.logger import LoggerCallback
|
|
from ray.tune.utils.callback import DEFAULT_CALLBACK_CLASSES
|
|
|
|
|
|
def _mock_record_from_module(module, monkeypatch):
|
|
recorded = {}
|
|
|
|
def mock_record_extra_usage_tag(key: TagKey, value: str):
|
|
recorded[key] = value
|
|
|
|
monkeypatch.setattr(
|
|
module,
|
|
"record_extra_usage_tag",
|
|
mock_record_extra_usage_tag,
|
|
)
|
|
return recorded
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_record(monkeypatch):
|
|
import ray.air._internal.usage
|
|
|
|
yield _mock_record_from_module(ray.air._internal.usage, monkeypatch=monkeypatch)
|
|
|
|
|
|
def train_fn(config):
|
|
train.report({"score": 1})
|
|
|
|
|
|
@pytest.fixture
|
|
def tuner(tmp_path):
|
|
yield tune.Tuner(train_fn, run_config=tune.RunConfig(storage_path=str(tmp_path)))
|
|
|
|
|
|
@pytest.fixture
|
|
def trainer(tmp_path):
|
|
from ray.train.data_parallel_trainer import DataParallelTrainer
|
|
|
|
yield DataParallelTrainer(
|
|
train_loop_per_worker=train_fn,
|
|
scaling_config=train.ScalingConfig(num_workers=2),
|
|
run_config=train.RunConfig(storage_path=str(tmp_path)),
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ray_start_4_cpus():
|
|
address_info = ray.init(num_cpus=4)
|
|
yield address_info
|
|
ray.shutdown()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"storage_path_filesystem_expected",
|
|
[
|
|
("/tmp/test", None, "local"),
|
|
("s3://", None, "s3"),
|
|
("gs://test", None, "gcs"),
|
|
("mock://test", None, "mock"),
|
|
("test", pyarrow.fs.LocalFileSystem(), "custom"),
|
|
],
|
|
)
|
|
def test_tag_storage_type(storage_path_filesystem_expected, mock_record, monkeypatch):
|
|
# Don't write anything to storage for the test.
|
|
monkeypatch.setattr(StorageContext, "_create_validation_file", lambda _: None)
|
|
monkeypatch.setattr(StorageContext, "_check_validation_file", lambda _: None)
|
|
|
|
storage_path, storage_filesystem, expected = storage_path_filesystem_expected
|
|
|
|
if Version(pyarrow.__version__) < Version("17.0.0") and storage_path.startswith(
|
|
"gs://"
|
|
):
|
|
pytest.skip("GCS support requires pyarrow >= 17.0.0")
|
|
|
|
storage = StorageContext(
|
|
storage_path=storage_path,
|
|
experiment_dir_name="test",
|
|
storage_filesystem=storage_filesystem,
|
|
)
|
|
air_usage.tag_storage_type(storage)
|
|
assert mock_record[TagKey.AIR_STORAGE_CONFIGURATION] == expected
|
|
|
|
|
|
class _CustomLoggerCallback(LoggerCallback):
|
|
pass
|
|
|
|
|
|
class _CustomCallback(Callback):
|
|
pass
|
|
|
|
|
|
_TEST_CALLBACKS = [
|
|
wandb.WandbLoggerCallback,
|
|
mlflow.MLflowLoggerCallback,
|
|
comet.CometLoggerCallback,
|
|
_CustomLoggerCallback,
|
|
_CustomLoggerCallback,
|
|
_CustomCallback,
|
|
]
|
|
|
|
|
|
def test_tag_setup_wandb(mock_record):
|
|
from ray.air.integrations.wandb import _setup_wandb
|
|
|
|
with patch.dict(os.environ, {wandb.WANDB_MODE_ENV_VAR: "disabled"}):
|
|
_setup_wandb(trial_id="a", trial_name="b", config={}, _wandb=MagicMock())
|
|
assert mock_record[TagKey.AIR_SETUP_WANDB_INTEGRATION_USED] == "1"
|
|
|
|
|
|
def test_tag_setup_mlflow(mock_record, monkeypatch):
|
|
from ray.air.integrations.mlflow import setup_mlflow
|
|
|
|
monkeypatch.setattr(ray.air.integrations.mlflow, "_MLflowLoggerUtil", MagicMock())
|
|
setup_mlflow()
|
|
assert mock_record[TagKey.AIR_SETUP_MLFLOW_INTEGRATION_USED] == "1"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"callback_classes_expected",
|
|
[
|
|
(None, None),
|
|
([], None),
|
|
([lambda: None], None),
|
|
(
|
|
DEFAULT_CALLBACK_CLASSES,
|
|
{cls.__name__: 1 for cls in DEFAULT_CALLBACK_CLASSES},
|
|
),
|
|
(
|
|
_TEST_CALLBACKS,
|
|
{
|
|
"WandbLoggerCallback": 1,
|
|
"MLflowLoggerCallback": 1,
|
|
"CometLoggerCallback": 1,
|
|
"CustomLoggerCallback": 2,
|
|
"CustomCallback": 1,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_tag_callbacks(mock_record, callback_classes_expected):
|
|
callback_classes, expected = callback_classes_expected
|
|
|
|
callbacks = (
|
|
[callback_cls() for callback_cls in callback_classes]
|
|
if callback_classes
|
|
else None
|
|
)
|
|
|
|
air_usage.tag_callbacks(callbacks)
|
|
|
|
callback_usage_str = mock_record.pop(TagKey.AIR_CALLBACKS, None)
|
|
callback_counts = json.loads(callback_usage_str) if callback_usage_str else None
|
|
assert callback_counts == expected
|
|
|
|
|
|
def test_tag_env_vars(ray_start_4_cpus, mock_record, tuner):
|
|
"""Test that env vars are recorded properly, and arbitrary user environment
|
|
variables are ignored."""
|
|
env_vars_to_record = {
|
|
"TUNE_GLOBAL_CHECKPOINT_S": "20",
|
|
"TUNE_MAX_PENDING_TRIALS_PG": "1",
|
|
}
|
|
untracked_env_vars = {"RANDOM_USER_ENV_VAR": "asdf"}
|
|
|
|
with patch.dict(os.environ, {**env_vars_to_record, **untracked_env_vars}):
|
|
tuner.fit()
|
|
|
|
recorded_env_vars = json.loads(mock_record[TagKey.AIR_ENV_VARS])
|
|
assert sorted(env_vars_to_record) == sorted(recorded_env_vars)
|
|
|
|
|
|
@pytest.mark.parametrize("entrypoint", list(AirEntrypoint))
|
|
def test_tag_air_entrypoint(ray_start_4_cpus, mock_record, entrypoint, tuner, trainer):
|
|
if entrypoint == AirEntrypoint.TUNE_RUN:
|
|
tune.run(train_fn)
|
|
elif entrypoint == AirEntrypoint.TUNE_RUN_EXPERIMENTS:
|
|
experiment_spec = Experiment("experiment", train_fn)
|
|
tune.run_experiments(experiments=experiment_spec)
|
|
elif entrypoint == AirEntrypoint.TUNER:
|
|
tuner.fit()
|
|
elif entrypoint == AirEntrypoint.TRAINER:
|
|
trainer.fit()
|
|
|
|
assert mock_record[TagKey.AIR_ENTRYPOINT] == entrypoint.value
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|