项目文件夹

文件
2026-07-13 13:22:34 +08:00

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)