mlflow--mlflow
272 行
10 KiB
Python
272 行
10 KiB
Python
import time
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
import mlflow
|
|
from mlflow import MlflowClient
|
|
from mlflow.exceptions import MlflowException
|
|
from mlflow.utils import _truncate_dict
|
|
from mlflow.utils.autologging_utils import MlflowAutologgingQueueingClient
|
|
from mlflow.utils.validation import (
|
|
MAX_ENTITY_KEY_LENGTH,
|
|
MAX_METRICS_PER_BATCH,
|
|
MAX_PARAM_VAL_LENGTH,
|
|
MAX_PARAMS_TAGS_PER_BATCH,
|
|
MAX_TAG_VAL_LENGTH,
|
|
)
|
|
|
|
|
|
def get_run_data(run_id):
|
|
client = MlflowClient()
|
|
data = client.get_run(run_id).data
|
|
# Ignore tags mlflow logs by default (e.g. "mlflow.user")
|
|
tags = {k: v for k, v in data.tags.items() if not k.startswith("mlflow.")}
|
|
return data.params, data.metrics, tags
|
|
|
|
|
|
def test_client_truncates_param_keys_and_values():
|
|
client = MlflowAutologgingQueueingClient()
|
|
params_to_log = {
|
|
"a" * (MAX_ENTITY_KEY_LENGTH + 5): "b" * (MAX_PARAM_VAL_LENGTH + 5),
|
|
"a" * (MAX_ENTITY_KEY_LENGTH + 50): "b" * (MAX_PARAM_VAL_LENGTH + 50),
|
|
}
|
|
|
|
with mlflow.start_run() as run:
|
|
client.log_params(run_id=run.info.run_id, params=params_to_log)
|
|
client.flush()
|
|
|
|
run_params = get_run_data(run.info.run_id)[0]
|
|
assert run_params == _truncate_dict(
|
|
params_to_log,
|
|
max_key_length=MAX_ENTITY_KEY_LENGTH,
|
|
max_value_length=MAX_PARAM_VAL_LENGTH,
|
|
)
|
|
|
|
|
|
def test_client_truncates_tag_keys_and_values():
|
|
client = MlflowAutologgingQueueingClient()
|
|
tags_to_log = {
|
|
"a" * (MAX_ENTITY_KEY_LENGTH + 5): "b" * (MAX_PARAM_VAL_LENGTH + 5),
|
|
"c" * (MAX_ENTITY_KEY_LENGTH + 50): "d" * (MAX_PARAM_VAL_LENGTH + 50),
|
|
}
|
|
|
|
with mlflow.start_run() as run:
|
|
client.set_tags(run_id=run.info.run_id, tags=tags_to_log)
|
|
client.flush()
|
|
|
|
run_tags = get_run_data(run.info.run_id)[2]
|
|
assert run_tags == _truncate_dict(
|
|
tags_to_log,
|
|
max_key_length=MAX_ENTITY_KEY_LENGTH,
|
|
max_value_length=MAX_TAG_VAL_LENGTH,
|
|
)
|
|
|
|
|
|
def test_client_truncates_metric_keys():
|
|
client = MlflowAutologgingQueueingClient()
|
|
metrics_to_log = {
|
|
"a" * (MAX_ENTITY_KEY_LENGTH + 5): 1,
|
|
"b" * (MAX_ENTITY_KEY_LENGTH + 50): 2,
|
|
}
|
|
|
|
with mlflow.start_run() as run:
|
|
client.log_metrics(run_id=run.info.run_id, metrics=metrics_to_log)
|
|
client.flush()
|
|
|
|
run_metrics = get_run_data(run.info.run_id)[1]
|
|
assert run_metrics == _truncate_dict(metrics_to_log, max_key_length=MAX_ENTITY_KEY_LENGTH)
|
|
|
|
|
|
def test_client_logs_expected_run_data():
|
|
client = MlflowAutologgingQueueingClient()
|
|
|
|
params_to_log = {
|
|
f"param_key_{i}": f"param_val_{i}" for i in range((2 * MAX_PARAMS_TAGS_PER_BATCH) + 1)
|
|
}
|
|
tags_to_log = {
|
|
f"tag_key_{i}": f"tag_val_{i}" for i in range((2 * MAX_PARAMS_TAGS_PER_BATCH) + 1)
|
|
}
|
|
metrics_to_log = {f"metric_key_{i}": i for i in range((4 * MAX_METRICS_PER_BATCH) + 1)}
|
|
|
|
with mlflow.start_run(run_name="my name") as run:
|
|
client.log_params(run_id=run.info.run_id, params=params_to_log)
|
|
client.set_tags(run_id=run.info.run_id, tags=tags_to_log)
|
|
client.log_metrics(run_id=run.info.run_id, metrics=metrics_to_log)
|
|
client.flush()
|
|
|
|
run_params, run_metrics, run_tags = get_run_data(run.info.run_id)
|
|
assert run_params == params_to_log
|
|
assert run_metrics == metrics_to_log
|
|
assert run_tags == tags_to_log
|
|
assert run.info.run_name == "my name"
|
|
|
|
|
|
def test_client_logs_metric_steps_correctly():
|
|
client = MlflowAutologgingQueueingClient()
|
|
|
|
with mlflow.start_run() as run:
|
|
for step in range(3):
|
|
client.log_metrics(run_id=run.info.run_id, metrics={"a": 1}, step=step)
|
|
client.flush()
|
|
|
|
metric_history = MlflowClient().get_metric_history(run_id=run.info.run_id, key="a")
|
|
assert len(metric_history) == 3
|
|
assert [metric.step for metric in metric_history] == list(range(3))
|
|
|
|
|
|
def test_client_run_creation_and_termination_are_successful():
|
|
experiment_name = "test_run_creation_termination"
|
|
MlflowClient().create_experiment(experiment_name)
|
|
experiment_id = MlflowClient().get_experiment_by_name(experiment_name).experiment_id
|
|
|
|
client = MlflowAutologgingQueueingClient()
|
|
pending_run_id = client.create_run(experiment_id=experiment_id, start_time=5, tags={"a": "b"})
|
|
client.set_terminated(run_id=pending_run_id, status="FINISHED", end_time=6)
|
|
client.flush()
|
|
|
|
runs = mlflow.search_runs(experiment_ids=[experiment_id], output_format="list")
|
|
assert len(runs) == 1
|
|
run = runs[0]
|
|
assert run.info.start_time == 5
|
|
assert run.info.end_time == 6
|
|
assert run.info.status == "FINISHED"
|
|
assert {"a": "b"}.items() <= run.data.tags.items()
|
|
|
|
|
|
def test_client_asynchronous_flush_operates_correctly():
|
|
original_log_batch = MlflowClient().log_batch
|
|
|
|
def mock_log_batch(run_id, metrics=(), params=(), tags=()):
|
|
# Sleep to simulate a long-running logging operation
|
|
time.sleep(3)
|
|
return original_log_batch(run_id, metrics, params, tags)
|
|
|
|
with mock.patch("mlflow.tracking.client.MlflowClient.log_batch") as log_batch_mock:
|
|
log_batch_mock.side_effect = mock_log_batch
|
|
|
|
with mlflow.start_run() as run:
|
|
client = MlflowAutologgingQueueingClient()
|
|
client.log_params(run_id=run.info.run_id, params={"a": "b"})
|
|
run_operations = client.flush(synchronous=False)
|
|
|
|
# Parameter data should not be available because the asynchronous logging
|
|
# operation is still inflight
|
|
logged_params_1 = get_run_data(run.info.run_id)[0]
|
|
assert not logged_params_1
|
|
|
|
run_operations.await_completion()
|
|
|
|
# Parameter data should now be available after waiting for completion of the
|
|
# asynchronous logging operation
|
|
logged_params_2 = get_run_data(run.info.run_id)[0]
|
|
assert logged_params_2 == {"a": "b"}
|
|
|
|
|
|
def test_client_synchronous_flush_operates_correctly():
|
|
original_log_batch = MlflowClient().log_batch
|
|
|
|
def mock_log_batch(run_id, metrics=(), params=(), tags=()):
|
|
# Sleep to simulate a long-running logging operation
|
|
time.sleep(3)
|
|
return original_log_batch(run_id, metrics, params, tags)
|
|
|
|
with mock.patch("mlflow.tracking.client.MlflowClient.log_batch") as log_batch_mock:
|
|
log_batch_mock.side_effect = mock_log_batch
|
|
|
|
with mlflow.start_run() as run:
|
|
client = MlflowAutologgingQueueingClient()
|
|
client.log_params(run_id=run.info.run_id, params={"a": "b"})
|
|
client.flush(synchronous=True)
|
|
|
|
# Parameter data should be available after the synchronous flush call returns
|
|
logged_params = get_run_data(run.info.run_id)[0]
|
|
assert logged_params == {"a": "b"}
|
|
|
|
|
|
def test_flush_clears_pending_operations():
|
|
with mock.patch("mlflow.tracking.client.MlflowClient", autospec=True) as mlflow_client_mock:
|
|
client = MlflowAutologgingQueueingClient()
|
|
|
|
pending_run_id = client.create_run(experiment_id=5)
|
|
client.log_params(run_id=pending_run_id, params={"a": "b"})
|
|
client.log_metrics(run_id=pending_run_id, metrics={"c": 1})
|
|
client.set_terminated(run_id=pending_run_id, status="FINISHED")
|
|
client.flush()
|
|
|
|
logging_call_count_1 = len(mlflow_client_mock.method_calls)
|
|
# Verify that at least 3 calls have been made to MLflow logging APIs as a result
|
|
# of the flush (i.e. log_batch, create_run, and set_terminated)
|
|
assert logging_call_count_1 >= 3
|
|
|
|
client.flush()
|
|
|
|
logging_call_count_2 = len(mlflow_client_mock.method_calls)
|
|
# Verify that performing a second flush did not result in any additional logging API calls,
|
|
# since no new run content was added prior to the flush
|
|
assert logging_call_count_2 == logging_call_count_1
|
|
|
|
|
|
def test_client_correctly_operates_as_context_manager_for_synchronous_flush():
|
|
params_to_log = {"a": "b"}
|
|
metrics_to_log = {"c": 1}
|
|
tags_to_log = {"d": "e"}
|
|
|
|
with mlflow.start_run(), MlflowAutologgingQueueingClient() as client:
|
|
run_id_1 = mlflow.active_run().info.run_id
|
|
client.log_params(run_id_1, params_to_log)
|
|
client.log_metrics(run_id_1, metrics_to_log)
|
|
client.set_tags(run_id_1, tags_to_log)
|
|
|
|
run_params_1, run_metrics_1, run_tags_1 = get_run_data(run_id_1)
|
|
assert run_params_1 == params_to_log
|
|
assert run_metrics_1 == metrics_to_log
|
|
assert run_tags_1 == tags_to_log
|
|
|
|
exc_to_raise = Exception("test exception")
|
|
with pytest.raises(Exception, match=str(exc_to_raise)) as raised_exc_info: # noqa PT012
|
|
with mlflow.start_run(), MlflowAutologgingQueueingClient() as client:
|
|
run_id_2 = mlflow.active_run().info.run_id
|
|
client.log_params(run_id_2, params_to_log)
|
|
client.log_metrics(run_id_2, metrics_to_log)
|
|
client.set_tags(run_id_2, tags_to_log)
|
|
raise exc_to_raise
|
|
|
|
assert raised_exc_info.value == exc_to_raise
|
|
# Verify that no run content was logged because the context exited with an exception
|
|
run_params_2, run_metrics_2, run_tags_2 = get_run_data(run_id_2)
|
|
assert not run_params_2
|
|
assert not run_metrics_2
|
|
assert not run_tags_2
|
|
|
|
|
|
def test_logging_failures_are_handled_as_expected():
|
|
experiment_name = "test_run_creation_termination"
|
|
MlflowClient().create_experiment(experiment_name)
|
|
experiment_id = MlflowClient().get_experiment_by_name(experiment_name).experiment_id
|
|
|
|
with mock.patch("mlflow.tracking.client.MlflowClient.log_batch") as log_batch_mock:
|
|
log_batch_mock.side_effect = Exception("Batch logging failed!")
|
|
|
|
client = MlflowAutologgingQueueingClient()
|
|
pending_run_id = client.create_run(experiment_id=experiment_id)
|
|
client.log_metrics(run_id=pending_run_id, metrics={"a": 1})
|
|
client.set_terminated(run_id=pending_run_id, status="KILLED")
|
|
|
|
with pytest.raises(MlflowException, match="Batch logging failed!") as exc:
|
|
client.flush()
|
|
|
|
runs = mlflow.search_runs(experiment_ids=[experiment_id], output_format="list")
|
|
assert len(runs) == 1
|
|
run = runs[0]
|
|
# Verify that metrics are absent due to the failure of batch logging
|
|
assert not run.data.metrics
|
|
# Verify that the run termination operation was still performed successfully
|
|
assert run.info.status == "KILLED"
|
|
|
|
assert (
|
|
f"Failed to perform one or more operations on the run with ID {run.info.run_id}"
|
|
in str(exc.value)
|
|
)
|
|
assert "Batch logging failed!" in str(exc.value)
|