Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bdffa0b
TST: Mark tests as thread-unsafe or limit the number of threads
seberg Jun 10, 2026
8caf6e6
TST: use tmp_path fixture in cufile (and mark some as unsafe)
seberg Jun 10, 2026
2b6fe34
TST: Move graph definnitions inline and mark "global" ones as thread-…
seberg Jun 10, 2026
2232f01
TST: Fixup memory tests, mostly work around issue when tearing down m…
seberg Jun 10, 2026
ba564dd
TST: Thread unsafe markers for test_managed_ops
seberg Jun 10, 2026
8c335e8
Avoid interactive backend when using run_tests.sh locally
seberg Jun 10, 2026
6757a01
Use indirect fixtures for a nicer pattern and avoid thread issues
seberg Jun 10, 2026
a9a2116
Make latch-kernel helper compile only once
seberg Jun 11, 2026
ea7cb70
Limit threads for event test that otherwise seems to ptentially fail
seberg Jul 8, 2026
2f90d7a
TST: Mark device-persistence-mode test as thread-unsafe
seberg Jul 8, 2026
e90369f
TST: Test fails in CI, assume that few enough threads eventually pass...
seberg Jul 14, 2026
9f142e9
TST: Add another sync to guard against potential deadlocks (seems I m…
seberg Jul 14, 2026
d4192a4
TST: Limit threads for another LatchKernel test
seberg Jul 14, 2026
443921a
TST: Force test_helpers to single threaded on windows to avoid crash
seberg Jul 14, 2026
a98fc52
TST: Many buffer related tests cannot run threaded on windows
seberg Jul 15, 2026
d920526
These tests seem to test process global cleanup (not sure I follow)
seberg Jul 29, 2026
0bde899
TST: Mark LatchKernel tests as thread-unsafe
seberg Jul 29, 2026
ca4c746
Merge branch 'main' into ft-testing-test-fixes
seberg Jul 29, 2026
bdc5f0f
Merge branch 'main' into ft-testing-test-fixes
seberg Aug 24, 2026
bc026aa
Adopt Andy's review suggestion for comment
seberg Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cuda_bindings/tests/test_cufile.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def ctx():
(err,) = cuda.cuCtxSetCurrent(ctx)
assert err == cuda.CUresult.CUDA_SUCCESS

yield
yield ctx

cuda.cuDevicePrimaryCtxRelease(device)

Expand Down
1 change: 1 addition & 0 deletions cuda_bindings/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_example(example):

env = os.environ.copy()
env["CUDA_BINDINGS_SKIP_EXAMPLE"] = "100"
env["MPLBACKEND"] = "Agg" # avoid plt.show() from blocking

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno if others get a pop-up for these tests, I did and with parallel testing, it might be a 100 windows :).


process = subprocess.run([sys.executable, example], capture_output=True, env=env) # noqa: S603
# returncode is a special value used in the examples to indicate that system requirements are not met.
Expand Down
119 changes: 71 additions & 48 deletions cuda_core/tests/graph/test_graph_definition.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions cuda_core/tests/graph/test_graph_definition_lifetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ def test_event_survives_graph_clone_and_execution(init_cuda):
# =============================================================================


@pytest.mark.thread_unsafe(reason="asserts cleanup on main thread")
@pytest.mark.agent_authored(model="gpt-5.6")
def test_user_object_cleanup_is_coalesced_on_python_thread(init_cuda):
"""More than 32 CUDA callbacks drain through one main-thread pending call."""
Expand Down Expand Up @@ -1412,6 +1413,7 @@ def test_memcpy_buffer_survives_close(init_cuda):
assert list(out) == [0xCD] * 4


@pytest.mark.thread_unsafe(reason="deferred cleanup on main thread which would wait")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are three tests here that fail with _wait_until @Andy-Jost. I just skipped them, the reason seems to be that cleanup can never happen as long as the main thread is waiting in thread.join()?

I suspect that is a potential but very minor issue (you would think eventually this cleanup happens). But wanted to make a comment. I didn't try to understand what is going on here exactly!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that deferred cleanup requires Python's main thread.

  1. pytest main thread: start workers -> join workers (blocking)
  2. pytest worker thread: del graph -> _wait_until
  3. CUDA calls back to Python from a separate thread and cuda.core schedules the destruction with Py_AddPendingCall
  4. Python's pending callbacks only run on the main thread.

pytest-run-parallel needs to give pending calls a chance to run. Instead of a blocking worker.join:

while any(worker.is_alive() for worker in workers):
    for worker in workers:
        worker.join(timeout=0.01)

The test also needs to be updated to use threading.main_thread().ident rather than threading.get_ident()

@pytest.mark.agent_authored(model="claude-opus-4.8")
def test_memcpy_buffer_allocations_released_after_graph_destroyed(init_cuda):
"""Destroying the graph frees both memcpy operand allocations.
Expand Down
33 changes: 22 additions & 11 deletions cuda_core/tests/graph/test_graph_memory_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""Tests for GraphMemoryResource allocation and attributes during graph capture."""

import pytest
from helpers.buffers import compare_buffer_to_constant, make_scratch_buffer, set_buffer
from helpers.buffers import compare_buffer_to_constant, make_scratch_buffer, set_buffer, thread_unsafe_on_windows
from helpers.memory import xfail_on_graph_mempool_oom

from cuda.core import (
Expand All @@ -21,6 +21,13 @@
from cuda.core.graph import GraphCompleteOptions
from cuda_python_test_helpers import IS_WINDOWS, IS_WSL

# NOTE(seberg): "global" mode seems thread-unsafe even when working on stream
_GRAPH_MODES = [
pytest.param("global", marks=pytest.mark.thread_unsafe(reason="gb instances share stream unsafely")),
"thread_local",
"relaxed",
]


def _common_kernels_alloc():
code = """
Expand Down Expand Up @@ -80,8 +87,9 @@ def free(self, buffers):
self.stream.sync()


@pytest.mark.parametrize("mode", ["no_graph", "global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", ["no_graph"] + _GRAPH_MODES)
@pytest.mark.parametrize("action", ["incr", "fill"])
@thread_unsafe_on_windows
def test_graph_alloc(mempool_device, mode, action):
"""Test basic graph capture with memory allocated and deallocated by
GraphMemoryResource.
Expand Down Expand Up @@ -130,7 +138,7 @@ def apply_kernels(mr, stream, out):
assert compare_buffer_to_constant(out, 3)
else:
# Capture work, then upload and launch.
gb = device.create_graph_builder().begin_building(mode)
gb = stream.create_graph_builder().begin_building(mode)
with xfail_on_graph_mempool_oom(device):
apply_kernels(mr=gmr, stream=gb, out=out)
graph = gb.end_building().complete()
Expand All @@ -150,7 +158,8 @@ def apply_kernels(mr, stream, out):


@pytest.mark.skipif(IS_WINDOWS or IS_WSL, reason="auto_free_on_launch not supported on Windows")
@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", _GRAPH_MODES)
@thread_unsafe_on_windows
def test_graph_alloc_with_output(mempool_device, mode):
"""Test for memory allocated in a graph being used outside the graph."""
NBYTES = 64
Expand All @@ -168,7 +177,7 @@ def test_graph_alloc_with_output(mempool_device, mode):
# Construct a graph to copy and increment the input. It returns a new
# buffer allocated within the graph. The auto_free_on_launch option
# is required to properly use the output buffer.
gb = device.create_graph_builder().begin_building(mode)
gb = stream.create_graph_builder().begin_building(mode)
with xfail_on_graph_mempool_oom(device):
out = gmr.allocate(NBYTES, stream=gb)
out.copy_from(in_, stream=gb)
Expand All @@ -195,7 +204,8 @@ def test_graph_alloc_with_output(mempool_device, mode):
assert compare_buffer_to_constant(out, 6)


@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", _GRAPH_MODES)
@pytest.mark.thread_unsafe(reason="gb instances share default stream")
def test_graph_mem_alloc_zero(mempool_device, mode):
device = mempool_device
gb = device.create_graph_builder().begin_building(mode)
Expand All @@ -213,7 +223,8 @@ def test_graph_mem_alloc_zero(mempool_device, mode):
assert buffer.device_id == int(device)


@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", _GRAPH_MODES)
@pytest.mark.thread_unsafe(reason="GMR is shared, so high mark is global")
def test_graph_mem_set_attributes(mempool_device, mode):
device = mempool_device
stream = device.create_stream()
Expand Down Expand Up @@ -265,7 +276,7 @@ def test_graph_mem_set_attributes(mempool_device, mode):
mman.reset()


@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", _GRAPH_MODES)
def test_gmr_check_capture_state(mempool_device, mode):
"""
Test expected errors (and non-errors) using GraphMemoryResource with graph
Expand All @@ -284,7 +295,7 @@ def test_gmr_check_capture_state(mempool_device, mode):
gmr.allocate(1, stream=stream)

# Capturing
gb = device.create_graph_builder().begin_building(mode=mode)
gb = stream.create_graph_builder().begin_building(mode=mode)
with xfail_on_graph_mempool_oom(device):
gmr.allocate(1, stream=gb) # no error
gb.end_building().complete()
Expand Down Expand Up @@ -320,7 +331,7 @@ def test_graph_memory_resource_attributes_repr(mempool_device):
assert "used_mem_high=" in r


@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
@pytest.mark.parametrize("mode", _GRAPH_MODES)
def test_dmr_check_capture_state(mempool_device, mode):
"""
Test expected errors (and non-errors) using DeviceMemoryResource with graph
Expand All @@ -334,7 +345,7 @@ def test_dmr_check_capture_state(mempool_device, mode):
dmr.allocate(1, stream=stream).close() # no error

# Capturing
gb = device.create_graph_builder().begin_building(mode=mode)
gb = stream.create_graph_builder().begin_building(mode=mode)
with pytest.raises(
RuntimeError,
match=r"cannot perform memory operations on a capturing "
Expand Down
1 change: 1 addition & 0 deletions cuda_core/tests/graph/test_graph_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def new_callback():
assert called == ["new"]


@pytest.mark.thread_unsafe(reason="deferred cleanup on main thread which would wait")
@pytest.mark.agent_authored(model="gpt-5.6")
def test_failed_graph_update_does_not_adopt_attachments(init_cuda):
"""A rejected source graph keeps ownership separate from the exec."""
Expand Down
14 changes: 13 additions & 1 deletion cuda_core/tests/helpers/buffers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

import ctypes

import pytest

from cuda.core import Buffer, Device, MemoryResource
from cuda.core._stream import Stream_accept
from cuda.core._utils.cuda_utils import driver, handle_return

from . import libc
from . import IS_WINDOWS, IS_WSL, libc

__all__ = [
"DummyDeviceMemoryResource",
Expand All @@ -18,9 +20,19 @@
"compare_equal_buffers",
"make_instrumented_memory_resource",
"make_scratch_buffer",
"thread_unsafe_on_windows",
]


def thread_unsafe_on_windows(func):
# Tests that use these buffers and access the memory on the host are
# thread-unsafe on windows. On windows the GPU must be fully quiescent for host
# access to be safe and with threaded tests that would require a barrier.
if IS_WINDOWS or IS_WSL:
return pytest.mark.thread_unsafe(reason="windows host-access unsafe while GPU is working")
return func


class StubMemoryResource(MemoryResource):
"""Device-only memory resource for tests that supply a fake pointer."""

Expand Down
2 changes: 1 addition & 1 deletion cuda_core/tests/helpers/latch.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import ctypes
Expand Down
6 changes: 6 additions & 0 deletions cuda_core/tests/memory/test_managed_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ def test_from_handle(self, init_cuda):
finally:
plain.close()

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_read_mostly_roundtrip(self, external_managed_buffer):
buf = external_managed_buffer
assert buf.read_mostly is False
Expand All @@ -373,6 +374,7 @@ def test_read_mostly_roundtrip(self, external_managed_buffer):
buf.read_mostly = False
assert buf.read_mostly is False

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_preferred_location_roundtrip(self, location_ops_device, external_managed_buffer):
device = location_ops_device
buf = external_managed_buffer
Expand All @@ -387,6 +389,7 @@ def test_preferred_location_roundtrip(self, location_ops_device, external_manage
buf.preferred_location = None
assert buf.preferred_location is None

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_preferred_location_roundtrip_host_numa(self, location_ops_device):
"""Host(numa_id=N) round-trips correctly on CUDA 13 builds."""
from cuda.core._utils.version import binding_version
Expand All @@ -407,6 +410,7 @@ def test_preferred_location_roundtrip_host_numa(self, location_ops_device):
finally:
plain.close()

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_accessed_by_add_discard(self, location_ops_device, external_managed_buffer):
device = location_ops_device
buf = external_managed_buffer
Expand All @@ -418,6 +422,7 @@ def test_accessed_by_add_discard(self, location_ops_device, external_managed_buf
buf.accessed_by.discard(device)
assert device not in buf.accessed_by

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_accessed_by_mutable_set_interface(self, location_ops_device, external_managed_buffer):
"""Full MutableSet conformance pass on AccessedBySetProxy.

Expand All @@ -437,6 +442,7 @@ def test_accessed_by_mutable_set_interface(self, location_ops_device, external_m
non_member=Host(numa_id=0),
)

@pytest.mark.thread_unsafe(reason="external_managed_buffer is shared between threads")
def test_accessed_by_set_assignment(self, location_ops_device, external_managed_buffer):
device = location_ops_device
buf = external_managed_buffer
Expand Down
1 change: 1 addition & 0 deletions cuda_core/tests/system/test_system_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ def test_c2c_mode_enabled(subtests):


@pytest.mark.skipif(helpers.IS_WSL or helpers.IS_WINDOWS, reason="Persistence mode not supported on WSL or Windows")
@pytest.mark.thread_unsafe(reason="device persistence mode is global state")
def test_persistence_mode_enabled(subtests):
for device in system.Device.get_all_devices():
with subtests.test(device_index=device.index):
Expand Down
2 changes: 2 additions & 0 deletions cuda_core/tests/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def test_error_timing_recorded():


@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)")
@pytest.mark.thread_unsafe(reason="requires a barrier wait to avoid overlapping pinned latch allocations")
def test_error_timing_incomplete():
device = Device()
device.set_current()
Expand Down Expand Up @@ -223,6 +224,7 @@ def test_event_ipc_descriptor_non_ipc(init_cuda):


@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)")
@pytest.mark.thread_unsafe(reason="requires a barrier wait to avoid overlapping pinned latch allocations")
def test_event_is_done_false(init_cuda):
"""Event.is_done returns False when captured work has not yet completed."""
device = Device()
Expand Down
5 changes: 4 additions & 1 deletion cuda_core/tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import types

import pytest
from helpers.buffers import PatternGen, compare_equal_buffers, make_scratch_buffer
from helpers.buffers import PatternGen, compare_equal_buffers, make_scratch_buffer, thread_unsafe_on_windows
from helpers.latch import LatchKernel
from helpers.logging import TimestampedLogger
from helpers.oom_diagnostics import (
Expand All @@ -29,6 +29,7 @@


@pytest.mark.skipif(Device().compute_capability.major < 7, reason="__nanosleep is only available starting Volta (sm70)")
@pytest.mark.thread_unsafe(reason="requires a barrier wait to avoid overlapping pinned latch allocations")
def test_latchkernel():
"""Test LatchKernel."""
log = TimestampedLogger(enabled=ENABLE_LOGGING)
Expand Down Expand Up @@ -63,6 +64,7 @@ def test_latchkernel():
under_compute_sanitizer(),
reason="Too slow under compute-sanitizer (UVM-heavy test).",
)
@thread_unsafe_on_windows
def test_patterngen_seeds():
"""Test PatternGen with seed argument."""
device = Device()
Expand All @@ -81,6 +83,7 @@ def test_patterngen_seeds():
pgen.verify_buffer(buffer, seed=j)


@thread_unsafe_on_windows
def test_patterngen_values():
"""Test PatternGen with value argument, also compare_equal_buffers."""
device = Device()
Expand Down
9 changes: 6 additions & 3 deletions cuda_core/tests/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
DummyUnifiedMemoryResource,
StubMemoryResource,
make_instrumented_memory_resource,
thread_unsafe_on_windows,
)
from helpers.constants import POOL_SIZE
from helpers.memory import (
Expand Down Expand Up @@ -283,9 +284,8 @@ def _pattern_bytes(value) -> bytes:


@pytest.fixture(params=["device", "unified", "pinned"])
def fill_env(request):
device = Device()
device.set_current()
def fill_env(request, init_cuda):
device = init_cuda
if request.param == "device":
mr = DummyDeviceMemoryResource(device)
elif request.param == "unified":
Expand Down Expand Up @@ -348,6 +348,7 @@ def fill_env(request):
)


@thread_unsafe_on_windows
@pytest.mark.parametrize("value,size,exc", _FILL_CASES)
def test_buffer_fill(fill_env, value, size, exc):
device, mr = fill_env
Expand Down Expand Up @@ -1519,6 +1520,8 @@ def test_managed_memory_resource_with_options(init_cuda):
device.sync()
dst_buffer.close()
src_buffer.close()
# TODO(seberg): 2026-06: mr close may be unsafe with incomplete `buf.close()`
device.sync()


def test_managed_memory_resource_preferred_location_default(init_cuda):
Expand Down
4 changes: 4 additions & 0 deletions cuda_core/tests/test_multiprocessing_warning.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import warnings
from unittest.mock import patch

import pytest
from helpers.constants import POOL_SIZE

from cuda.core import DeviceMemoryResource, DeviceMemoryResourceOptions, EventOptions
Expand All @@ -20,6 +21,9 @@
from cuda.core._memory._ipc import _reduce_allocation_handle
from cuda.core._utils.cuda_utils import check_multiprocessing_start_method, reset_fork_warning

# We could move these to a (session) fixtures
pytestmark = pytest.mark.thread_unsafe(reason="all tests use unittest.mock.patch")


def test_warn_on_fork_method_device_memory_resource(ipc_device):
"""Test that warning is emitted when DeviceMemoryResource is pickled with fork method."""
Expand Down
Loading
Loading