Skip to content

Commit da060a2

Browse files
committed
[Test] Parametrize teardown-no-double-raise test with force_sync; route via pytest.raises/nullcontext
1 parent 527a4ca commit da060a2

1 file changed

Lines changed: 32 additions & 9 deletions

File tree

tests/python/test_adstack.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,18 +1117,23 @@ def test_adstack_overflow_multithreaded():
11171117
qd.sync()
11181118

11191119

1120-
def test_adstack_overflow_during_teardown_does_not_abort(tmp_path):
1120+
@pytest.mark.parametrize("force_sync", [False, True])
1121+
def test_adstack_overflow_caught_then_clean_teardown(tmp_path, force_sync):
11211122
# This test runs the kernel in a child process (not via `@test_utils.test`, which iterates arches), so it
11221123
# cannot rely on the decorator's `require=qd.extension.adstack` skip. Guard manually: skip if the CPU backend
11231124
# was not built with the adstack extension, matching what the sibling overflow tests get from the decorator.
11241125
if not is_extension_supported(qd.cpu, qd.extension.adstack):
11251126
pytest.skip("adstack extension not available on cpu")
11261127

1127-
# Pins the teardown-no-double-raise contract for `Program::finalize()`. The per-launch
1128+
# Pins the per-launch-raise + clean-teardown contract for `Program::finalize()`. The per-launch
11281129
# `check_adstack_overflow_and_assert()` poll wired into `Program::launch_kernel` surfaces an overflow at
1129-
# the very next kernel-launch entry rather than waiting for `qd.sync()`. The user catches the resulting
1130-
# `QuadrantsAssertionError` and exits cleanly. Even after the catch, the two teardown `synchronize()`
1131-
# calls inside `Program::finalize()` re-enter the LLVM `check_adstack_overflow_and_assert` path, and
1130+
# the very next kernel-launch entry on synchronous backends (CPU). On async backends (CUDA / AMDGPU /
1131+
# Metal / Vulkan) the kernel may still be in flight when `launch_kernel` returns, so the post-launch poll
1132+
# reads a not-yet-set flag - the overflow is then surfaced at the next `qd.sync()` via the post-drain
1133+
# check in `LlvmProgramImpl::synchronize_and_assert` (or the host-mapped readback in
1134+
# `GfxRuntime::synchronize`). The `force_sync` parametrisation toggles whether the user issues an
1135+
# explicit `qd.sync()`. Either way the teardown contract holds: the two teardown `synchronize()` calls
1136+
# inside `Program::finalize()` re-enter the LLVM `check_adstack_overflow_and_assert` path, and
11321137
# `LlvmProgramImpl::pre_finalize()` must have set `finalizing_ = true` early enough that the per-launch
11331138
# poll AND the `synchronize_and_assert` poll BOTH short-circuit during the destructor. If either path
11341139
# re-raised, the destructor would `std::terminate()` instead of returning a clean exit code (-6 / SIGABRT
@@ -1141,7 +1146,10 @@ def test_adstack_overflow_during_teardown_does_not_abort(tmp_path):
11411146
# the runtime overflow flag in debug mode. Without the flag set there is no flag for the teardown
11421147
# guard to swallow, and the bug this test pins cannot trigger.
11431148
child_script = textwrap.dedent(
1144-
"""
1149+
f"""
1150+
from contextlib import nullcontext
1151+
1152+
import pytest
11451153
import quadrants as qd
11461154
11471155
qd.init(arch=qd.cpu, ad_stack_experimental_enabled=True, ad_stack_size=32, debug=True)
@@ -1164,10 +1172,25 @@ def compute():
11641172
compute()
11651173
y.grad[None] = 1.0
11661174
x.grad[0] = 0.0
1167-
try:
1175+
1176+
# CPU is the only arch the child runs on for now; synchronous-backend semantics apply.
1177+
# The per-launch poll wired into `Program::launch_kernel` surfaces the overflow at `compute.grad()`.
1178+
# On a future async-arch parametrisation, flip `is_sync_backend` and the qd.sync() branch becomes
1179+
# the raising path while compute.grad() drops to nullcontext.
1180+
is_sync_backend = True
1181+
force_sync = {force_sync}
1182+
1183+
def raises_overflow():
1184+
return pytest.raises((AssertionError, RuntimeError), match=r"[Aa]dstack overflow")
1185+
1186+
with raises_overflow() if is_sync_backend else nullcontext():
11681187
compute.grad()
1169-
except (AssertionError, RuntimeError) as e:
1170-
assert "adstack overflow" in str(e).lower()
1188+
if force_sync:
1189+
with raises_overflow() if not is_sync_backend else nullcontext():
1190+
qd.sync()
1191+
# Process exits without `qd.sync()` (when force_sync=False). Teardown's two `synchronize()` calls
1192+
# plus their per-launch polls must short-circuit on `finalizing_`; otherwise the destructor
1193+
# double-raises and the process exits non-zero / SIGABRTs.
11711194
"""
11721195
)
11731196
script_path = tmp_path / "overflow_teardown_child.py"

0 commit comments

Comments
 (0)