Skip to content

Commit 55cb344

Browse files
authored
test(cuda.core): isolate pending-call saturation cleanup (#2406)
* test(cuda.core): isolate pending-call saturation cleanup Run the process-global queue probe in a subprocess and explicitly drain its synthetic callbacks so parallel free-threaded tests cannot contaminate one another. * test(cuda.core): keep subprocess probe self-contained Pass the finalization timeout into the child so process isolation does not depend on importing test helpers from the temporary working directory.
1 parent 22b7ddc commit 55cb344

1 file changed

Lines changed: 86 additions & 37 deletions

File tree

cuda_core/tests/graph/test_graph_definition_lifetime.py

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -611,48 +611,97 @@ def test_user_object_cleanup_is_coalesced_on_python_thread(init_cuda):
611611

612612

613613
@pytest.mark.agent_authored(model="gpt-5.6")
614-
def test_pending_call_queue_saturation_preserves_cleanup(init_cuda):
614+
def test_pending_call_queue_saturation_preserves_cleanup(tmp_path):
615615
"""A full CPython queue neither strands nor mis-threads cleanup."""
616-
pending_callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
617-
add_pending_call = ctypes.pythonapi.Py_AddPendingCall
618-
add_pending_call.argtypes = [pending_callback_type, ctypes.c_void_p]
619-
add_pending_call.restype = ctypes.c_int
616+
code = f"timeout = {_FINALIZE_TIMEOUT!r}\n" + textwrap.dedent(
617+
"""
618+
import ctypes
619+
import gc
620+
import threading
621+
import time
622+
623+
from cuda.core import Device
624+
from cuda.core.graph import GraphDefinition
620625
621-
@pending_callback_type
622-
def noop_pending_call(_):
623-
return 0
626+
class ThreadRecordingCallback:
627+
def __init__(self, finalized_threads):
628+
self.finalized_threads = finalized_threads
624629
625-
finalized_threads = []
626-
main_thread = threading.get_ident()
627-
first_callback = _ThreadRecordingCallback(finalized_threads)
628-
first_graph = GraphDefinition()
629-
first_graph.callback(first_callback)
630-
graph_holder = [first_graph]
631-
worker_done = threading.Event()
632-
queue_was_full = []
630+
def __call__(self):
631+
pass
632+
633+
def __del__(self):
634+
self.finalized_threads.append(threading.get_ident())
635+
636+
def wait_until(predicate):
637+
deadline = time.monotonic() + timeout
638+
while True:
639+
gc.collect()
640+
if predicate():
641+
return
642+
if time.monotonic() >= deadline:
643+
break
644+
time.sleep(0)
645+
time.sleep(0.02)
646+
raise AssertionError(f"condition not satisfied within {timeout}s")
647+
648+
Device(0).set_current()
649+
650+
pending_callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
651+
add_pending_call = ctypes.pythonapi.Py_AddPendingCall
652+
add_pending_call.argtypes = [pending_callback_type, ctypes.c_void_p]
653+
add_pending_call.restype = ctypes.c_int
654+
make_pending_calls = ctypes.pythonapi.Py_MakePendingCalls
655+
make_pending_calls.argtypes = []
656+
make_pending_calls.restype = ctypes.c_int
657+
658+
@pending_callback_type
659+
def noop_pending_call(_):
660+
return 0
661+
662+
finalized_threads = []
663+
main_thread = threading.get_ident()
664+
first_callback = ThreadRecordingCallback(finalized_threads)
665+
first_graph = GraphDefinition()
666+
first_graph.callback(first_callback)
667+
graph_holder = [first_graph]
668+
worker_done = threading.Event()
669+
queue_was_full = []
670+
671+
del first_callback, first_graph
672+
673+
def fill_queue_and_destroy():
674+
while add_pending_call(noop_pending_call, None) == 0:
675+
pass
676+
queue_was_full.append(True)
677+
graph_holder.clear()
678+
worker_done.set()
633679
634-
del first_callback, first_graph
680+
worker = threading.Thread(target=fill_queue_and_destroy)
681+
worker.start()
682+
assert worker_done.wait(timeout=5)
683+
worker.join()
684+
assert queue_was_full == [True]
635685
636-
def fill_queue_and_destroy():
637-
while add_pending_call(noop_pending_call, None) == 0:
638-
pass
639-
queue_was_full.append(True)
640-
graph_holder.clear()
641-
worker_done.set()
642-
643-
worker = threading.Thread(target=fill_queue_and_destroy)
644-
worker.start()
645-
assert worker_done.wait(timeout=5)
646-
worker.join()
647-
assert queue_was_full == [True]
648-
649-
# A later safe cuda-core close retries after the main thread has had an
650-
# opportunity to drain the foreign pending calls.
651-
retry_builder = Device().create_graph_builder()
652-
retry_builder.close()
653-
654-
_wait_until(lambda: len(finalized_threads) == 1)
655-
assert set(finalized_threads) == {main_thread}
686+
# Free space before the cuda-core entry that retries cleanup scheduling.
687+
assert make_pending_calls() == 0
688+
689+
retry_builder = Device().create_graph_builder()
690+
retry_builder.close()
691+
692+
wait_until(lambda: len(finalized_threads) == 1)
693+
assert set(finalized_threads) == {main_thread}
694+
"""
695+
)
696+
result = subprocess.run( # noqa: S603 - controlled interpreter probe
697+
[sys.executable, "-c", code],
698+
capture_output=True,
699+
text=True,
700+
timeout=60,
701+
# Isolate the process-global pending-call queue from parallel tests.
702+
cwd=tmp_path,
703+
)
704+
assert result.returncode == 0, result.stderr
656705

657706

658707
@pytest.mark.agent_authored(model="gpt-5.6")

0 commit comments

Comments
 (0)