Files
mlflow--mlflow/tests/utils/test_async_artifacts_logging_queue.py
T
2026-07-13 13:22:34 +08:00

281 lines
9.1 KiB
Python

import io
import pickle
import random
import threading
import time
import pytest
from PIL import Image
from mlflow import MlflowException
from mlflow.utils.async_logging.async_artifacts_logging_queue import AsyncArtifactsLoggingQueue
TOTAL_ARTIFACTS = 5
class RunArtifacts:
def __init__(self, throw_exception_on_artifact_number=None):
if throw_exception_on_artifact_number is None:
throw_exception_on_artifact_number = []
self.received_run_id = ""
self.received_artifacts = []
self.received_filenames = []
self.received_artifact_paths = []
self.artifact_count = 0
self.throw_exception_on_artifact_number = throw_exception_on_artifact_number or []
def consume_queue_data(self, filename, artifact_path, artifact):
self.artifact_count += 1
if self.artifact_count in self.throw_exception_on_artifact_number:
raise MlflowException("Failed to log run data")
self.received_artifacts.append(artifact)
self.received_filenames.append(filename)
self.received_artifact_paths.append(artifact_path)
def _get_run_artifacts(total_artifacts=TOTAL_ARTIFACTS):
for num in range(0, total_artifacts):
filename = f"image_{num}.png"
artifact_path = f"images/artifact_{num}"
artifact = Image.new("RGB", (100, 100), color="red")
yield filename, artifact_path, artifact
def _assert_sent_received_artifacts(
filenames_sent,
artifact_paths_sent,
artifacts_sent,
received_filenames,
received_artifact_paths,
received_artifacts,
):
for num in range(1, len(filenames_sent)):
assert filenames_sent[num] == received_filenames[num]
for num in range(1, len(artifact_paths_sent)):
assert artifact_paths_sent[num] == received_artifact_paths[num]
for num in range(1, len(artifacts_sent)):
assert artifacts_sent[num] == received_artifacts[num]
def test_single_thread_publish_consume_queue():
run_artifacts = RunArtifacts()
async_logging_queue = AsyncArtifactsLoggingQueue(run_artifacts.consume_queue_data)
async_logging_queue.activate()
filenames_sent = []
artifact_paths_sent = []
artifacts_sent = []
for filename, artifact_path, artifact in _get_run_artifacts():
async_logging_queue.log_artifacts_async(
filename=filename, artifact_path=artifact_path, artifact=artifact
)
filenames_sent.append(filename)
artifact_paths_sent.append(artifact_path)
artifacts_sent.append(artifact)
async_logging_queue.flush()
_assert_sent_received_artifacts(
filenames_sent,
artifact_paths_sent,
artifacts_sent,
run_artifacts.received_filenames,
run_artifacts.received_artifact_paths,
run_artifacts.received_artifacts,
)
def test_queue_activation():
run_artifacts = RunArtifacts()
async_logging_queue = AsyncArtifactsLoggingQueue(run_artifacts.consume_queue_data)
assert not async_logging_queue._is_activated
for filename, artifact_path, artifact in _get_run_artifacts(1):
with pytest.raises(MlflowException, match="AsyncArtifactsLoggingQueue is not activated."):
async_logging_queue.log_artifacts_async(
filename=filename, artifact_path=artifact_path, artifact=artifact
)
async_logging_queue.activate()
assert async_logging_queue._is_activated
def test_partial_logging_failed():
run_data = RunArtifacts(throw_exception_on_artifact_number=[3, 4])
async_logging_queue = AsyncArtifactsLoggingQueue(run_data.consume_queue_data)
async_logging_queue.activate()
filenames_sent = []
artifact_paths_sent = []
artifacts_sent = []
run_operations = []
batch_id = 1
for filename, artifact_path, artifact in _get_run_artifacts():
if batch_id in [3, 4]:
with pytest.raises(MlflowException, match="Failed to log run data"):
async_logging_queue.log_artifacts_async(
filename=filename, artifact_path=artifact_path, artifact=artifact
).wait()
else:
run_operations.append(
async_logging_queue.log_artifacts_async(
filename=filename, artifact_path=artifact_path, artifact=artifact
)
)
filenames_sent.append(filename)
artifact_paths_sent.append(artifact_path)
artifacts_sent.append(artifact)
batch_id += 1
for run_operation in run_operations:
run_operation.wait()
_assert_sent_received_artifacts(
filenames_sent,
artifact_paths_sent,
artifacts_sent,
run_data.received_filenames,
run_data.received_artifact_paths,
run_data.received_artifacts,
)
def test_publish_multithread_consume_single_thread():
run_data = RunArtifacts(throw_exception_on_artifact_number=[])
async_logging_queue = AsyncArtifactsLoggingQueue(run_data.consume_queue_data)
async_logging_queue.activate()
def _send_artifact(run_data_queueing_processor, run_operations=None):
if run_operations is None:
run_operations = []
filenames_sent = []
artifact_paths_sent = []
artifacts_sent = []
for filename, artifact_path, artifact in _get_run_artifacts():
run_operations.append(
run_data_queueing_processor.log_artifacts_async(
filename=filename, artifact_path=artifact_path, artifact=artifact
)
)
time.sleep(random.randint(1, 3))
filenames_sent.append(filename)
artifact_paths_sent.append(artifact_path)
artifacts_sent.append(artifact)
run_operations = []
t1 = threading.Thread(
name="test-async-artifacts-1",
target=_send_artifact,
args=(async_logging_queue, run_operations),
)
t2 = threading.Thread(
name="test-async-artifacts-2",
target=_send_artifact,
args=(async_logging_queue, run_operations),
)
t1.start()
t2.start()
t1.join()
t2.join()
for run_operation in run_operations:
run_operation.wait()
assert len(run_data.received_filenames) == 2 * TOTAL_ARTIFACTS
assert len(run_data.received_artifact_paths) == 2 * TOTAL_ARTIFACTS
assert len(run_data.received_artifacts) == 2 * TOTAL_ARTIFACTS
class Consumer:
def __init__(self) -> None:
self.filenames = []
self.artifact_paths = []
self.artifacts = []
self.barrier = threading.Event()
def consume_queue_data(self, filename, artifact_path, artifact):
self.barrier.wait()
self.filenames.append(filename)
self.artifact_paths.append(artifact_path)
self.artifacts.append(artifact)
def __getstate__(self):
state = self.__dict__.copy()
del state["barrier"]
return state
def __setstate__(self, state):
self.__dict__.update(state)
self.barrier = threading.Event()
def test_async_logging_queue_pickle():
consumer = Consumer()
async_logging_queue = AsyncArtifactsLoggingQueue(consumer.consume_queue_data)
# Pickle the queue without activating it.
buffer = io.BytesIO()
pickle.dump(async_logging_queue, buffer)
deserialized_queue = pickle.loads(buffer.getvalue()) # Type: AsyncArtifactsLoggingQueue
# Activate the queue and submit 10 items. Workers block on the barrier,
# so the consumer's state remains empty during pickling.
async_logging_queue.activate()
run_operations = [
async_logging_queue.log_artifacts_async(
filename=f"image-{val}.png",
artifact_path="images/image-artifact.png",
artifact=Image.new("RGB", (100, 100), color="blue"),
)
for val in range(0, 10)
]
# Pickle while workers are blocked — consumer state is deterministically empty.
buffer = io.BytesIO()
pickle.dump(async_logging_queue, buffer)
deserialized_queue = pickle.loads(buffer.getvalue()) # Type: AsyncLoggingQueue
assert deserialized_queue._queue.empty()
assert deserialized_queue._lock is not None
assert deserialized_queue._is_activated is False
# Release workers and wait for all operations to complete.
consumer.barrier.set()
for run_operation in run_operations:
run_operation.wait()
assert len(consumer.filenames) == 10
# Activate the deserialized queue and submit 10 more items.
# The deserialized consumer is a separate copy with an empty filenames list.
deserialized_consumer = deserialized_queue._artifact_logging_func.__self__
deserialized_consumer.barrier.set()
deserialized_queue.activate()
assert deserialized_queue._is_activated
run_operations = []
for val in range(0, 10):
run_operations.append(
deserialized_queue.log_artifacts_async(
filename=f"image2-{val}.png",
artifact_path="images/image-artifact2.png",
artifact=Image.new("RGB", (100, 100), color="green"),
)
)
for run_operation in run_operations:
run_operation.wait()
assert len(deserialized_consumer.filenames) == 10