Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions ci/test_cuda_compute_minimal_python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,28 @@ if [[ "${py_version}" == "3.14t" ]]; then
compute/test_free_threading_stress.py \
compute/test_multi_cc_serialization.py::test_aot_build_result_load_failure_is_shared_and_retryable \
compute/test_multi_cc_serialization.py::test_aot_serialization_waits_for_canonical_first_load

# Broad thread-safety sweep (pytest-run-parallel): re-run the numba-free
# functional suite with each test executed concurrently across threads
# (barrier-synchronized start), stressing the process-wide build cache,
# single-flight coordination, and the Cython bindings from many threads at
# once. Complements test_free_threading_stress.py above, which targets specific
# shared-object scenarios by hand. -n 0 so the threads share one interpreter.
#
# --parallel-threads=2 matches CuPy's free-threading CI (the closest GPU
# precedent); a small fixed count bounds GPU-memory pressure from concurrent
# kernels and stays reproducible across runners, unlike =auto (the runner's
# logical-core count).
#
# pytest-run-parallel is only used by this sweep, so install it on the 3.14t
# path rather than for every minimal (e.g. non-free-threaded 3.14) run.
python -m pip install pytest-run-parallel

# Fail fast if the interpreter is not actually GIL-free (wrong build /
# PYTHON_GIL=1): pytest-run-parallel does NOT catch a GIL that is enabled from
# the start -- it would run threads GIL-serialized and pass vacuously. (A GIL
# *re-enabled mid-run* by a non-free-threaded import IS caught by the plugin,
# which is why we do not pass --ignore-gil-enabled.)
python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is enabled; parallel sweep has no signal'"
python -m pytest -n 0 -v --parallel-threads=2 compute/test_no_numba.py
fi
26 changes: 23 additions & 3 deletions python/cuda_cccl/tests/compute/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,21 @@ def cuda_stream() -> Generator[Stream, None, None]:


@pytest.fixture(scope="function", autouse=True)
def verify_sass(request, monkeypatch):
def verify_sass(request):
if request.node.get_closest_marker("no_verify_sass"):
return

if not check_ldl_stl_in_sass:
print("not checking sass")
return

# Pull monkeypatch dynamically rather than as a fixture parameter so this
# autouse fixture does not add monkeypatch to every test's static fixture
# closure. pytest-run-parallel treats monkeypatch as thread-unsafe based on
# that closure, so a parameter here would serialize the entire free-threaded
# parallel sweep -- even though this fixture only patches on the opt-in
# SASS-check path (check_ldl_stl_in_sass, off by default and in CI).
monkeypatch = request.getfixturevalue("monkeypatch")

import cuda.compute._cccl_interop

monkeypatch.setattr(
Expand Down Expand Up @@ -124,8 +131,21 @@ def pytest_collection_modifyitems(config, items):
serialization_skip = pytest.mark.skip(
reason="serialization not supported on v2 (HostJIT) backend"
)
# Under the pytest-run-parallel sweep (--parallel-threads > 1) skip the
# blanket raise_on_numba_import injection: it monkeypatches
# builtins.__import__, which pytest-run-parallel treats as thread-unsafe and
# would serialize every no_numba test, neutering the sweep.
#
# getoption returns the int 1 from the argparse default but a *string* for
# CLI-passed values ("2", "auto"). Only bypass for an explicit numeric thread
# count > 1 (what CI passes): "auto" is left to inject the guard so a run the
# plugin executes single-threaded (e.g. "auto" resolving to 1 logical CPU) is
# not mistaken for a parallel run. (A bare `> 1` would also TypeError on the
# "2" string.)
parallel_threads = str(config.getoption("parallel_threads", 1))
running_parallel = parallel_threads.isdigit() and int(parallel_threads) > 1
for item in items:
if item.get_closest_marker("no_numba"):
if item.get_closest_marker("no_numba") and not running_parallel:
if "raise_on_numba_import" not in item.fixturenames:
item.fixturenames.append("raise_on_numba_import")
if USING_V2 and item.get_closest_marker("serialization"):
Expand Down
18 changes: 9 additions & 9 deletions python/cuda_cccl/tests/compute/test_no_numba.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ def _raw_negate_i16_op() -> RawOp:
return _raw_op(source, "no_numba_negate_i16")


def test_import_numba_raises():
def test_import_numba_raises(raise_on_numba_import):
# Request the guard explicitly rather than relying on the no_numba
# auto-injection (skipped under the parallel sweep, see conftest
# pytest_collection_modifyitems): this test exercises the guard directly, and
# the monkeypatch it depends on keeps it single-threaded under the sweep.
with pytest.raises(
ImportError, match="This test is marked 'no_numba' but attempted to import it"
):
Expand Down Expand Up @@ -205,9 +209,8 @@ def test_binary_search_explicit_opkind_less(search, side):
np.testing.assert_array_equal(d_out.copy_to_host(), expected)


def test_segmented_reduce_well_known_plus(monkeypatch):
monkeypatch.setattr(cuda.compute._cccl_interop, "_check_sass", False)

@pytest.mark.no_verify_sass
def test_segmented_reduce_well_known_plus():
h_input = np.asarray([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint32)
h_starts = np.asarray([0, 3, 5], dtype=np.int32)
h_ends = np.asarray([3, 5, 8], dtype=np.int32)
Expand Down Expand Up @@ -288,11 +291,8 @@ def test_segmented_sort_keys():
np.testing.assert_array_equal(d_output.copy_to_host(), expected)


def test_unique_by_key_well_known_equal_to(monkeypatch):
cc_major, _ = cuda.compute._cccl_interop.CudaDevice().compute_capability
if cc_major >= 9:
monkeypatch.setattr(cuda.compute._cccl_interop, "_check_sass", False)

@pytest.mark.no_verify_sass
def test_unique_by_key_well_known_equal_to():
Comment thread
NaderAlAwar marked this conversation as resolved.
h_keys = np.asarray([1, 1, 2, 2, 2, 3, 4, 4], dtype=np.int16)
h_values = np.asarray([10, 11, 20, 21, 22, 30, 40, 41], dtype=np.int8)
d_keys = DeviceArray.from_numpy(h_keys)
Expand Down
Loading