blob: df38db27e90a1014071508c0864f13d6deb78c99 [file] [edit]
# RUN: %PYTHON %s
"""Tests that PassManager.run detaches from the Python interpreter.
The core test runs two PassManagers, on independent Contexts, in two threads.
Each pipeline contains the native `test-python-rendezvous` pass (defined in
lib/TestConcurrencyPasses.cpp), which blocks inside runOnOperation until both
threads have arrived at a shared native barrier. The barrier can only be
satisfied while both threads execute native pass code at the same time:
* If PassManager.run releases the GIL (or the interpreter is free-threaded),
both threads enter their pipelines, meet at the barrier, and both runs
succeed.
* If PassManager.run holds the GIL, whichever thread enters run() first blocks
natively while still attached to the interpreter, the second thread can
never start its own run(), and the first thread fails deterministically
once the barrier times out.
No part of this relies on timing margins: the failure mode is a guaranteed
timeout, and the success mode only requires that a runnable thread is
eventually scheduled, which the barrier waits for.
"""
import threading
import uuid
from mlir.ir import Context, MLIRError, Module
from mlir.passmanager import PassManager
import mlir.dialects.python_test as python_test
python_test.register_rendezvous_pass()
def unique_barrier_id(prefix):
"""Barriers are process-global and one-shot, so give each use a fresh id."""
return f"{prefix}-{uuid.uuid4().hex}"
def run_rendezvous(barrier_id, timeout_seconds, participants=2, ctx=None):
"""Runs one rendezvous pipeline on the given (default: fresh) Context."""
with Context() if ctx is None else ctx:
module = Module.parse("module {}")
pm = PassManager.parse(
"builtin.module(test-python-rendezvous{"
f"barrier-id={barrier_id} "
f"participants={participants} "
f"timeout-seconds={timeout_seconds}}})"
)
pm.run(module.operation)
# Sanity-check the failure mode first: a lone participant must time out, the
# pipeline must fail, and the pass's diagnostic must be captured and attached
# to the MLIRError. This also exercises the ErrorCapture diagnostic path.
def testSoloRendezvousTimesOut():
barrier_id = unique_barrier_id("solo")
try:
run_rendezvous(barrier_id, timeout_seconds=1)
raise AssertionError("expected the pipeline to fail")
except MLIRError as e:
messages = [d.message for d in e.error_diagnostics]
assert any(
f"rendezvous '{barrier_id}' timed out" in m for m in messages
), messages
# The core test: two PassManagers on independent Contexts must be able to
# execute native pass code simultaneously.
def testConcurrentPassManagersRendezvous():
barrier_id = unique_barrier_id("pair")
errors = [None, None]
def worker(idx):
try:
run_rendezvous(barrier_id, timeout_seconds=20)
except Exception as e: # pylint: disable=broad-except
errors[idx] = e
threads = [threading.Thread(target=worker, args=(i,)) for i in range(2)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=60)
assert not any(t.is_alive() for t in threads), "worker thread hung"
if any(errors):
raise AssertionError(
"two PassManagers could not execute concurrently; PassManager.run "
f"is not releasing the GIL: {errors}"
)
# A user-attached diagnostic handler must work while the calling thread is
# detached from the interpreter inside PassManager.run: the handler trampoline
# re-attaches around everything it does (creating the Diagnostic object,
# invoking the callback, and tearing the object down afterwards). With
# emit_error_diagnostics set, the pipeline failure diagnostic bypasses the
# internal error capture and reaches the handler instead.
def testDiagnosticHandlerDuringDetachedRun():
seen = []
def handler(d):
# Materialize everything, including the notes tuple, so that the
# diagnostic's teardown after the callback has Python objects to drop.
seen.append((d.severity, str(d.message), [str(n) for n in d.notes]))
return True
with Context() as ctx:
ctx.emit_error_diagnostics = True
handle = ctx.attach_diagnostic_handler(handler)
barrier_id = unique_barrier_id("diag")
try:
run_rendezvous(barrier_id, timeout_seconds=1, ctx=ctx)
raise AssertionError("expected the pipeline to fail")
except MLIRError as e:
# The handler consumed the diagnostic, so nothing was captured.
assert not e.error_diagnostics, e.error_diagnostics
finally:
handle.detach()
assert len(seen) == 1, seen
assert f"rendezvous '{barrier_id}' timed out" in seen[0][1], seen
assert not handle.had_error
if __name__ == "__main__":
testSoloRendezvousTimesOut()
testConcurrentPassManagersRendezvous()
testDiagnosticHandlerDuringDetachedRun()