Files
2026-07-13 13:22:34 +08:00

727 lines
28 KiB
Python

import gc
import random
import threading
import time
from datetime import datetime
from time import sleep
from typing import Any
from unittest.mock import MagicMock, call, patch
import numpy as np
import pytest
from optuna.distributions import CategoricalDistribution, FloatDistribution
from optuna.storages import BaseStorage
from optuna.storages._base import DEFAULT_STUDY_NAME_PREFIX
from optuna.study._frozen import FrozenStudy
from optuna.study._study_direction import StudyDirection
from optuna.trial import FrozenTrial, TrialState
import mlflow
from mlflow.entities import Metric, Param, RunTag
from mlflow.optuna.storage import MlflowStorage
ALL_STATES = list(TrialState)
EXAMPLE_ATTRS: dict[str, Any] = {
"dataset": "MNIST",
"none": None,
"json_serializable": {"baseline_score": 0.001, "tags": ["image", "classification"]},
}
def _setup_studies(
storage: BaseStorage,
n_study: int,
n_trial: int,
seed: int,
direction: StudyDirection = None,
) -> tuple[dict[int, FrozenStudy], dict[int, dict[int, FrozenTrial]]]:
generator = random.Random(seed)
study_id_to_frozen_study: dict[int, FrozenStudy] = {}
study_id_to_trials: dict[int, dict[int, FrozenTrial]] = {}
for i in range(n_study):
study_name = "test-study-name-{}".format(i)
if direction is None:
direction = generator.choice([StudyDirection.MINIMIZE, StudyDirection.MAXIMIZE])
study_id = storage.create_new_study(directions=(direction,), study_name=study_name)
storage.set_study_user_attr(study_id, "u", i)
storage.set_study_system_attr(study_id, "s", i)
trials = {}
for j in range(n_trial):
trial = _generate_trial(generator)
trial.number = j
trial._trial_id = storage.create_new_trial(study_id, trial)
trials[trial._trial_id] = trial
study_id_to_trials[study_id] = trials
study_id_to_frozen_study[study_id] = FrozenStudy(
study_name=study_name,
direction=direction,
user_attrs={"u": i},
system_attrs={"s": i},
study_id=study_id,
)
return study_id_to_frozen_study, study_id_to_trials
def _generate_trial(generator: random.Random) -> FrozenTrial:
example_params = {
"paramA": (generator.uniform(0, 1), FloatDistribution(0, 1)),
"paramB": (generator.uniform(1, 2), FloatDistribution(1, 2, log=True)),
"paramC": (
generator.choice(["CatA", "CatB", "CatC"]),
CategoricalDistribution(("CatA", "CatB", "CatC")),
),
"paramD": (generator.uniform(-3, 0), FloatDistribution(-3, 0)),
"paramE": (generator.choice([0.1, 0.2]), CategoricalDistribution((0.1, 0.2))),
}
example_attrs = {
"attrA": "valueA",
"attrB": 1,
"attrC": None,
"attrD": {"baseline_score": 0.001, "tags": ["image", "classification"]},
}
state = generator.choice(ALL_STATES)
params = {}
distributions = {}
user_attrs = {}
system_attrs: dict[str, Any] = {}
intermediate_values = {}
for key, (value, dist) in example_params.items():
if generator.choice([True, False]):
params[key] = value
distributions[key] = dist
for key, value in example_attrs.items():
if generator.choice([True, False]):
user_attrs["usr_" + key] = value
if generator.choice([True, False]):
system_attrs["sys_" + key] = value
for i in range(generator.randint(4, 10)):
if generator.choice([True, False]):
intermediate_values[i] = generator.uniform(-10, 10)
return FrozenTrial(
number=0, # dummy
state=state,
value=generator.uniform(-10, 10),
datetime_start=datetime.now(),
datetime_complete=datetime.now() if state.is_finished() else None,
params=params,
distributions=distributions,
user_attrs=user_attrs,
system_attrs=system_attrs,
intermediate_values=intermediate_values,
trial_id=0, # dummy
)
@pytest.fixture
def setup_storage(db_uri):
mlflow.set_tracking_uri(db_uri)
experiment_id = mlflow.create_experiment(name="optuna_mlflow_test")
storage = MlflowStorage(
experiment_id=experiment_id, batch_flush_interval=1.0, batch_size_threshold=5
)
storage._flush_thread = MagicMock()
yield storage
mlflow.delete_experiment(experiment_id)
def test_queue_batch_operation_creates_new_queue_for_new_run(setup_storage):
storage = setup_storage
run_id = "test-run-id"
test_metric = Metric("test_metric", 1.0, int(time.time() * 1000), 0)
# Call the method with a new run_id
storage._queue_batch_operation(run_id, metrics=[test_metric])
# Check that a new queue was created for this run_id
assert run_id in storage._batch_queue
assert len(storage._batch_queue[run_id]["metrics"]) == 1
assert storage._batch_queue[run_id]["metrics"][0] == test_metric
assert len(storage._batch_queue[run_id]["params"]) == 0
assert len(storage._batch_queue[run_id]["tags"]) == 0
def test_queue_batch_operation_appends_to_existing_queue(setup_storage):
storage = setup_storage
run_id = "test-run-id"
# Setup existing queue
storage._batch_queue[run_id] = {"metrics": [], "params": [], "tags": []}
# Add metrics, params, and tags
test_metric = Metric("test_metric", 1.0, int(time.time() * 1000), 0)
test_param = Param("test_param", "value")
test_tag = RunTag("test_tag", "value")
# Queue each type separately
storage._queue_batch_operation(run_id, metrics=[test_metric])
storage._queue_batch_operation(run_id, params=[test_param])
storage._queue_batch_operation(run_id, tags=[test_tag])
# Check all were added correctly
assert len(storage._batch_queue[run_id]["metrics"]) == 1
assert len(storage._batch_queue[run_id]["params"]) == 1
assert len(storage._batch_queue[run_id]["tags"]) == 1
def test_flush_batch_sends_data_to_mlflow(setup_storage):
with patch("mlflow.optuna.storage.MlflowClient") as mock_client:
storage = setup_storage
run_id = "test-run-id"
mock_client_instance = MagicMock()
mock_client.return_value = mock_client_instance
storage._mlflow_client = mock_client
# Setup batch data
metrics = [Metric("test_metric", 1.0, int(time.time() * 1000), 0)]
params = [Param("test_param", "value")]
tags = [RunTag("test_tag", "value")]
storage._batch_queue[run_id] = {"metrics": metrics[:], "params": params[:], "tags": tags[:]}
# Call _flush_batch
storage._flush_batch(run_id)
# Verify MLflow client was called with the correct data
storage._mlflow_client.log_batch.assert_called_once_with(
run_id, metrics=metrics, params=params, tags=tags
)
# Verify batch was cleared
assert len(storage._batch_queue[run_id]["metrics"]) == 0
assert len(storage._batch_queue[run_id]["params"]) == 0
assert len(storage._batch_queue[run_id]["tags"]) == 0
def test_flush_batch_does_nothing_for_empty_batch(setup_storage):
with patch("mlflow.optuna.storage.MlflowClient") as mock_client:
storage = setup_storage
run_id = "test-run-id"
# Create a mock instance that will be returned when MlflowClient is instantiated
mock_client_instance = MagicMock()
mock_client.return_value = mock_client_instance
storage._mlflow_client = mock_client
# Setup empty batch
storage._batch_queue[run_id] = {"metrics": [], "params": [], "tags": []}
# Call _flush_batch
storage._flush_batch(run_id)
# Verify MLflow client was not called
storage._mlflow_client.log_batch.assert_not_called()
def test_flush_batch_handles_nonexistent_run(setup_storage):
with patch("mlflow.optuna.storage.MlflowClient") as mock_client:
storage = setup_storage
run_id = "nonexistent-run"
# Create a mock instance that will be returned when MlflowClient is instantiated
mock_client_instance = MagicMock()
mock_client.return_value = mock_client_instance
storage._mlflow_client = mock_client
# Call _flush_batch for a run that doesn't exist in the queue
storage._flush_batch(run_id)
# Verify MLflow client was not called
storage._mlflow_client.log_batch.assert_not_called()
def test_flush_all_batches_flushes_all_runs(setup_storage):
storage = setup_storage
# Setup multiple runs with data
run_ids = ["run1", "run2", "run3"]
for run_id in run_ids:
storage._batch_queue[run_id] = {
"metrics": [Metric(f"m_{run_id}", 1.0, int(time.time() * 1000), 0)],
"params": [Param(f"p_{run_id}", "value")],
"tags": [RunTag(f"t_{run_id}", "value")],
}
# Create a spy on _flush_batch
with patch.object(storage, "_flush_batch") as mock_flush:
# Call flush_all_batches
storage.flush_all_batches()
# Check that _flush_batch was called for each run
expected_calls = [call(run_id) for run_id in run_ids]
mock_flush.assert_has_calls(expected_calls, any_order=True)
assert mock_flush.call_count == len(run_ids)
def test_flush_all_batches_handles_empty_queue(setup_storage):
storage = setup_storage
# Ensure batch queue is empty
storage._batch_queue = {}
# Create a spy on _flush_batch
with patch.object(storage, "_flush_batch") as mock_flush:
# Call flush_all_batches
storage.flush_all_batches()
# Check that _flush_batch was not called
mock_flush.assert_not_called()
def test_create_new_study(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
frozen_studies = storage.get_all_studies()
assert len(frozen_studies) == 1
assert frozen_studies[0]._study_id == study_id
assert frozen_studies[0].study_name.startswith(DEFAULT_STUDY_NAME_PREFIX)
study_id2 = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
# Study id must be unique.
assert study_id != study_id2
frozen_studies = storage.get_all_studies()
assert len(frozen_studies) == 2
assert {s._study_id for s in frozen_studies} == {study_id, study_id2}
assert all(s.study_name.startswith(DEFAULT_STUDY_NAME_PREFIX) for s in frozen_studies)
def test_delete_study(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
storage.create_new_trial(study_id)
trials = storage.get_all_trials(study_id)
assert len(trials) == 1
with pytest.raises(mlflow.exceptions.MlflowException, match="Run .* not found"):
# Deletion of non-existent study.
storage.delete_study(study_id + "1")
storage.delete_study(study_id)
def test_get_study_id_from_name_and_get_study_name_from_id(setup_storage):
storage = setup_storage
study_name = "test_optuna_mlflow_study"
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE], study_name=study_name)
# Test existing study.
assert storage.get_study_name_from_id(study_id) == study_name
assert storage.get_study_id_from_name(study_name) == study_id
# Test not existing study.
with pytest.raises(Exception, match="Study dummy-name not found"):
storage.get_study_id_from_name("dummy-name")
with pytest.raises(mlflow.exceptions.MlflowException, match="Run .* not found"):
storage.get_study_name_from_id(study_id + "1")
def test_set_and_get_study_directions(setup_storage):
storage = setup_storage
for target in [
(StudyDirection.MINIMIZE,),
(StudyDirection.MAXIMIZE,),
(StudyDirection.MINIMIZE, StudyDirection.MAXIMIZE),
(StudyDirection.MAXIMIZE, StudyDirection.MINIMIZE),
[StudyDirection.MINIMIZE, StudyDirection.MAXIMIZE],
[StudyDirection.MAXIMIZE, StudyDirection.MINIMIZE],
]:
study_id = storage.create_new_study(directions=target)
def check_get() -> None:
got_directions = storage.get_study_directions(study_id)
assert got_directions == list(target), (
"Direction of a study should be a tuple of `StudyDirection` objects."
)
# Test setting value.
check_get()
# Test non-existent study.
non_existent_study_id = study_id + "1"
with pytest.raises(mlflow.exceptions.MlflowException, match="Run .* not found"):
storage.get_study_directions(non_existent_study_id)
def test_set_and_get_study_user_attrs(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
def check_set_and_get(key: str, value: Any) -> None:
storage.set_study_user_attr(study_id, key, value)
assert storage.get_study_user_attrs(study_id)[key] == value
# Test setting value.
for key, value in EXAMPLE_ATTRS.items():
check_set_and_get(key, value)
assert storage.get_study_user_attrs(study_id) == EXAMPLE_ATTRS
# Test overwriting value.
check_set_and_get("dataset", "ImageNet")
# Non-existent study id.
non_existent_study_id = study_id + "1"
with pytest.raises(mlflow.exceptions.MlflowException, match="Run .* not found"):
storage.get_study_user_attrs(non_existent_study_id)
# Non-existent study id.
with pytest.raises(mlflow.exceptions.MlflowException, match="Run .* not found"):
storage.set_study_user_attr(non_existent_study_id, "key", "value")
def test_set_and_get_study_system_attrs(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
def check_set_and_get(key: str, value: Any) -> None:
storage.set_study_system_attr(study_id, key, value)
assert storage.get_study_system_attrs(study_id)[key] == value
# Test setting value.
for key, value in EXAMPLE_ATTRS.items():
check_set_and_get(key, value)
assert storage.get_study_system_attrs(study_id) == EXAMPLE_ATTRS
# Test overwriting value.
check_set_and_get("dataset", "ImageNet")
def test_create_new_trial(setup_storage):
storage = setup_storage
def _check_trials(
trials: list[FrozenTrial],
idx: int,
trial_id: int,
time_before_creation: datetime,
time_after_creation: datetime,
) -> None:
assert len(trials) == idx + 1
assert len({t._trial_id for t in trials}) == idx + 1
assert trial_id in {t._trial_id for t in trials}
assert {t.number for t in trials} == set(range(idx + 1))
assert all(t.state == TrialState.RUNNING for t in trials)
assert all(t.params == {} for t in trials)
assert all(t.intermediate_values == {} for t in trials)
assert all(t.user_attrs == {} for t in trials)
assert all(t.system_attrs == {} for t in trials)
assert all(
t.datetime_start < time_before_creation
for t in trials
if t._trial_id != trial_id and t.datetime_start is not None
)
assert all(
time_before_creation < t.datetime_start < time_after_creation
for t in trials
if t._trial_id == trial_id and t.datetime_start is not None
)
assert all(t.value is None for t in trials)
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
n_trial_in_study = 3
for i in range(n_trial_in_study):
time_before_creation = datetime.now()
sleep(0.001) # Sleep 1ms to avoid faulty assertion on Windows OS.
trial_id = storage.create_new_trial(study_id)
sleep(0.001)
time_after_creation = datetime.now()
trials = storage.get_all_trials(study_id)
_check_trials(trials, i, trial_id, time_before_creation, time_after_creation)
def test_create_new_trial_with_template_trial(setup_storage):
storage = setup_storage
start_time = datetime.now()
end_time = datetime.now()
template_trial = FrozenTrial(
state=TrialState.COMPLETE,
value=10000,
datetime_start=start_time,
datetime_complete=end_time,
params={"x": 0.5},
distributions={"x": FloatDistribution(0, 1)},
user_attrs={"foo": "bar"},
system_attrs={"baz": 123},
intermediate_values={1: 10, 2: 100, 3: 1000},
number=55, # This entry is ignored.
trial_id=-1, # dummy value (unused).
)
def _check_trials(trials: list[FrozenTrial], idx: int, trial_id: int) -> None:
assert len(trials) == idx + 1
assert len({t._trial_id for t in trials}) == idx + 1
assert trial_id in {t._trial_id for t in trials}
assert {t.number for t in trials} == set(range(idx + 1))
assert all(t.state == template_trial.state for t in trials)
assert all(t.params == template_trial.params for t in trials)
assert all(t.distributions == template_trial.distributions for t in trials)
assert all(t.intermediate_values == template_trial.intermediate_values for t in trials)
assert all(t.user_attrs == template_trial.user_attrs for t in trials)
assert all(t.system_attrs == template_trial.system_attrs for t in trials)
# assert all(t.datetime_start == template_trial.datetime_start for t in trials)
# assert all(t.datetime_complete == template_trial.datetime_complete for t in trials)
assert all(t.value == template_trial.value for t in trials)
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
n_trial_in_study = 3
for i in range(n_trial_in_study):
trial_id = storage.create_new_trial(study_id, template_trial=template_trial)
trials = storage.get_all_trials(study_id)
_check_trials(trials, i, trial_id)
def test_get_trial_number_from_id(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
trial_id = storage.create_new_trial(study_id)
assert storage.get_trial_number_from_id(trial_id) == 0
trial_id = storage.create_new_trial(study_id)
assert storage.get_trial_number_from_id(trial_id) == 1
def test_set_trial_state_values_for_state(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
trial_ids = [storage.create_new_trial(study_id) for _ in ALL_STATES]
for trial_id, state in zip(trial_ids, ALL_STATES):
if state == TrialState.WAITING:
continue
assert storage.get_trial(trial_id).state == TrialState.RUNNING
datetime_start_prev = storage.get_trial(trial_id).datetime_start
storage.set_trial_state_values(
trial_id, state=state, values=(0.0,) if state.is_finished() else None
)
assert storage.get_trial(trial_id).state == state
# Repeated state changes to RUNNING should not trigger further datetime_start changes.
if state == TrialState.RUNNING:
assert storage.get_trial(trial_id).datetime_start == datetime_start_prev
if state.is_finished():
assert storage.get_trial(trial_id).datetime_complete is not None
else:
assert storage.get_trial(trial_id).datetime_complete is None
def test_get_trial_param_and_get_trial_params(setup_storage):
storage = setup_storage
_, study_to_trials = _setup_studies(storage, n_study=2, n_trial=5, seed=1)
for _, trial_id_to_trial in study_to_trials.items():
for trial_id, expected_trial in trial_id_to_trial.items():
assert storage.get_trial_params(trial_id) == expected_trial.params
for key in expected_trial.params.keys():
assert storage.get_trial_param(trial_id, key) == expected_trial.distributions[
key
].to_internal_repr(expected_trial.params[key])
def test_set_trial_param(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
trial_id_1 = storage.create_new_trial(study_id)
trial_id_2 = storage.create_new_trial(study_id)
# Setup distributions.
distribution_x = FloatDistribution(low=1.0, high=2.0)
distribution_y_1 = CategoricalDistribution(choices=("Shibuya", "Ebisu", "Meguro"))
distribution_z = FloatDistribution(low=1.0, high=100.0, log=True)
# Set new params.
storage.set_trial_param(trial_id_1, "x", 0.5, distribution_x)
storage.set_trial_param(trial_id_1, "y", 2, distribution_y_1)
assert storage.get_trial_param(trial_id_1, "x") == 0.5
assert storage.get_trial_param(trial_id_1, "y") == 2
# Check set_param breaks neither get_trial nor get_trial_params.
assert storage.get_trial(trial_id_1).params == {"x": 0.5, "y": "Meguro"}
assert storage.get_trial_params(trial_id_1) == {"x": 0.5, "y": "Meguro"}
# Set params to another trial.
storage.set_trial_param(trial_id_2, "x", 0.3, distribution_x)
storage.set_trial_param(trial_id_2, "z", 0.1, distribution_z)
assert storage.get_trial_param(trial_id_2, "x") == 0.3
assert storage.get_trial_param(trial_id_2, "z") == 0.1
assert storage.get_trial(trial_id_2).params == {"x": 0.3, "z": 0.1}
assert storage.get_trial_params(trial_id_2) == {"x": 0.3, "z": 0.1}
storage.set_trial_state_values(trial_id_2, state=TrialState.COMPLETE)
# Cannot assign params to finished trial.
with pytest.raises(RuntimeError, match="Trial#1 has already finished and can not be updated."):
storage.set_trial_param(trial_id_2, "y", 2, distribution_y_1)
# Check the previous call does not change the params.
with pytest.raises(KeyError, match="'param_internal_val_y'"):
storage.get_trial_param(trial_id_2, "y")
# State should be checked prior to distribution compatibility.
with pytest.raises(RuntimeError, match="Trial#1 has already finished and can not be updated."):
storage.set_trial_param(trial_id_2, "y", 0.4, distribution_z)
def test_set_trial_state_values_for_values(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
trial_id_1 = storage.create_new_trial(study_id)
trial_id_2 = storage.create_new_trial(study_id)
trial_id_3 = storage.create_new_trial(
storage.create_new_study(directions=[StudyDirection.MINIMIZE])
)
trial_id_4 = storage.create_new_trial(study_id)
trial_id_5 = storage.create_new_trial(study_id)
# Test setting new value.
storage.set_trial_state_values(trial_id_1, state=TrialState.COMPLETE, values=(0.5,))
storage.set_trial_state_values(trial_id_3, state=TrialState.COMPLETE, values=(float("inf"),))
storage.set_trial_state_values(trial_id_4, state=TrialState.WAITING, values=(0.1, 0.2, 0.3))
storage.set_trial_state_values(trial_id_5, state=TrialState.WAITING, values=[0.1, 0.2, 0.3])
assert storage.get_trial(trial_id_1).value == 0.5
assert storage.get_trial(trial_id_2).value is None
# SQLite/SQLAlchemy sanitizes +inf to max float value
assert storage.get_trial(trial_id_3).value == 1.7976931348623157e308
assert storage.get_trial(trial_id_4).values == [0.1, 0.2, 0.3]
assert storage.get_trial(trial_id_5).values == [0.1, 0.2, 0.3]
def test_set_trial_intermediate_value(setup_storage):
storage = setup_storage
study_id = storage.create_new_study(directions=[StudyDirection.MINIMIZE])
trial_id_1 = storage.create_new_trial(study_id)
trial_id_2 = storage.create_new_trial(study_id)
trial_id_3 = storage.create_new_trial(
storage.create_new_study(directions=[StudyDirection.MINIMIZE])
)
trial_id_4 = storage.create_new_trial(study_id)
# Test setting new values.
storage.set_trial_intermediate_value(trial_id_1, 0, 0.3)
storage.set_trial_intermediate_value(trial_id_1, 2, 0.4)
storage.set_trial_intermediate_value(trial_id_3, 0, 0.1)
storage.set_trial_intermediate_value(trial_id_3, 1, 0.4)
storage.set_trial_intermediate_value(trial_id_3, 2, 0.5)
storage.set_trial_intermediate_value(trial_id_3, 3, float("inf"))
storage.set_trial_intermediate_value(trial_id_4, 0, float("nan"))
assert storage.get_trial(trial_id_1).intermediate_values == {0: 0.3, 2: 0.4}
assert storage.get_trial(trial_id_2).intermediate_values == {}
# SQLite/SQLAlchemy sanitizes +inf to max float value
assert storage.get_trial(trial_id_3).intermediate_values == {
0: 0.1,
1: 0.4,
2: 0.5,
3: 1.7976931348623157e308,
}
assert np.isnan(storage.get_trial(trial_id_4).intermediate_values[0])
# Test setting existing step.
storage.set_trial_intermediate_value(trial_id_1, 0, 0.2)
assert storage.get_trial(trial_id_1).intermediate_values == {0: 0.2, 2: 0.4}
def test_get_trial_user_attrs(setup_storage):
storage = setup_storage
_, study_to_trials = _setup_studies(storage, n_study=2, n_trial=5, seed=10)
assert all(
storage.get_trial_user_attrs(trial_id) == trial.user_attrs
for trials in study_to_trials.values()
for trial_id, trial in trials.items()
)
def test_set_trial_user_attr(setup_storage):
storage = setup_storage
trial_id_1 = storage.create_new_trial(
storage.create_new_study(directions=[StudyDirection.MINIMIZE])
)
def check_set_and_get(trial_id: int, key: str, value: Any) -> None:
storage.set_trial_user_attr(trial_id, key, value)
assert storage.get_trial(trial_id).user_attrs[key] == value
# Test setting value.
for key, value in EXAMPLE_ATTRS.items():
check_set_and_get(trial_id_1, key, value)
assert storage.get_trial(trial_id_1).user_attrs == EXAMPLE_ATTRS
# Test overwriting value.
check_set_and_get(trial_id_1, "dataset", "ImageNet")
def test_set_trial_system_attr(setup_storage):
storage = setup_storage
trial_id_1 = storage.create_new_trial(
storage.create_new_study(directions=[StudyDirection.MINIMIZE])
)
def check_set_and_get(trial_id: int, key: str, value: Any) -> None:
storage.set_trial_system_attr(trial_id, key, value)
assert storage.get_trial_system_attrs(trial_id)[key] == value
# Test setting value.
for key, value in EXAMPLE_ATTRS.items():
check_set_and_get(trial_id_1, key, value)
system_attrs = storage.get_trial(trial_id_1).system_attrs
assert system_attrs == EXAMPLE_ATTRS
# Test overwriting value.
check_set_and_get(trial_id_1, "dataset", "ImageNet")
def test_get_n_trials(setup_storage):
storage = setup_storage
study_id_to_frozen_studies, _ = _setup_studies(storage, n_study=2, n_trial=7, seed=50)
for study_id in study_id_to_frozen_studies:
assert storage.get_n_trials(study_id) == 7
def test_study_exists_method(setup_storage):
storage = setup_storage
# Test non-existent study
assert not storage.get_study_id_by_name_if_exists("non-existent-study")
# Create a study
storage.create_new_study([StudyDirection.MINIMIZE], "test-study")
# Test existing study
assert storage.get_study_id_by_name_if_exists("test-study")
def test_get_study_id_by_name_if_exists(setup_storage):
storage = setup_storage
# Test non-existent study
assert storage.get_study_id_by_name_if_exists("non-existent") is None
# Create a study
study_id = storage.create_new_study([StudyDirection.MINIMIZE], "test-study")
# Test existing study
result = storage.get_study_id_by_name_if_exists("test-study")
assert result == study_id
def test_flush_threads_exit_after_gc():
def _flush_threads():
return [t for t in threading.enumerate() if "mlflow_optuna_batch_flush_worker" in t.name]
threads_before = set(_flush_threads())
def _create_instances():
for _ in range(5):
MlflowStorage(experiment_id="1")
_create_instances()
gc.collect()
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
new_threads = [t for t in _flush_threads() if t not in threads_before]
if not new_threads:
break
time.sleep(0.1)
else:
raise TimeoutError(f"Flush threads still alive after 3s: {new_threads}")