Skip to content

Commit c17a310

Browse files
committed
CI: use local Toolkit libraries in local core tests
— codex GPT-6 on behalf of Ralf — Separate local and wheel test dependency groups so CuPy test setup does not install component wheels that shadow the local Toolkit. Keep the existing pinned dependencies for wheel-based jobs. Include cuRAND in the shared mini Toolkit and recognize a loadable native cuRAND library for bare sample requirements. Versioned, extras and URL requirements continue to use distribution metadata. The component-list change refreshes the mini-Toolkit caches for all action consumers. Signed-off-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent 3a2a871 commit c17a310

6 files changed

Lines changed: 136 additions & 22 deletions

File tree

‎.github/actions/fetch_ctk/action.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ inputs:
1414
cuda-components:
1515
description: "A list of the CTK components to install as a comma-separated list. e.g. 'cuda_nvcc,cuda_nvrtc,cuda_cudart'"
1616
required: false
17-
default: "cuda_nvcc,cuda_cudart,cuda_crt,libnvvm,cuda_nvrtc,cuda_profiler_api,cuda_cccl,cuda_cupti,libnvjitlink,libcufile,libnvfatbin,libcudla"
17+
default: "cuda_nvcc,cuda_cudart,cuda_crt,libnvvm,cuda_nvrtc,cuda_profiler_api,cuda_cccl,cuda_cupti,libnvjitlink,libcufile,libnvfatbin,libcudla,libcurand"
1818
cuda-path:
1919
description: "where the CTK components will be installed to, relative to $PWD"
2020
required: false

‎ci/tools/run-tests‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,26 +145,26 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then
145145
echo "Installing bindings (source: ${BINDINGS_SOURCE})"
146146
pip install "${BINDINGS_ARGS[@]}"
147147
echo "Installing core wheel"
148-
# Constrain cuda-toolkit to the requested CTK version to avoid
149-
# pip pulling in a newer nvidia-cuda-runtime that conflicts with it.
150-
# Skip the constraint for a local CTK of the build's own (newest) minor:
151-
# its toolkit wheels may not be on PyPI yet, and the local CTK is what the
152-
# job tests.
148+
# Local jobs get CuPy's headers and libraries from the mini CTK; installing
149+
# component wheels would take precedence over those local libraries.
150+
CORE_TEST_GROUP="test-cu${TEST_CUDA_MAJOR}"
153151
TOOLKIT_PIN=("cuda-toolkit==${TEST_CUDA_MAJOR_MINOR}.*")
154-
if [[ "${LOCAL_CTK}" == 1 && "${TEST_CUDA_MAJOR}" == "${BUILD_CUDA_MAJOR}" && "${TEST_CUDA_MINOR}" == "${BUILD_CUDA_MINOR}" ]]; then
152+
if [[ "${LOCAL_CTK}" == 1 ]]; then
153+
CORE_TEST_GROUP+="-local"
155154
TOOLKIT_PIN=()
156155
fi
157-
pip install "${CORE_WHL[@]}" --group "test-cu${TEST_CUDA_MAJOR}" "${TEST_FT_GROUP[@]}" "${TOOLKIT_PIN[@]}"
156+
pip install "${CORE_WHL[@]}" --group "${CORE_TEST_GROUP}" "${TEST_FT_GROUP[@]}" "${TOOLKIT_PIN[@]}"
158157
# Optional deps needed by some cuda.core samples exercised in this suite.
159158
# CuPy already comes from the
160159
# test-cuXX group; install the rest best-effort so those samples run
161160
# instead of being waived. Samples with heavier/unavailable deps stay waived.
162161
echo "Installing optional sample deps (nvtx, pillow)"
163162
pip install nvtx pillow || echo "Warning: optional sample deps install failed; affected samples will be waived"
164-
# curand: some samples (vectorAdd, parallelHistogram) call cp.random,
165-
# which dlopens libcurand at runtime.
166-
echo "Installing optional sample deps (curand)"
167-
pip install "cuda-toolkit[curand]==${TEST_CUDA_MAJOR_MINOR}.*" || echo "Warning: curand install failed; affected samples will be waived"
163+
# Local jobs already get cuRAND from the mini CTK.
164+
if [[ "${LOCAL_CTK}" != 1 ]]; then
165+
echo "Installing optional sample deps (curand)"
166+
pip install "cuda-toolkit[curand]==${TEST_CUDA_MAJOR_MINOR}.*" || echo "Warning: curand install failed; affected samples will be waived"
167+
fi
168168
echo "Installed packages before core tests:"
169169
pip list
170170
echo "Running core tests"

‎ci/tools/tests/test_run_tests.py‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import shutil
99
import subprocess
1010
import sys
11+
import tomllib
1112
import zipfile
1213
from pathlib import Path
1314

@@ -97,6 +98,17 @@ def run(*, mode="core", source="floor", floor="13.4.1", cuda_version="13.0.2", l
9798
for args in installs
9899
if any(arg.startswith("cuda-core[") or Path(arg).name.startswith("cuda_core-") for arg in args)
99100
)
101+
if mode == "core":
102+
major = cuda_version.split(".")[0]
103+
group = core_install[core_install.index("--group") + 1]
104+
assert group == f"test-cu{major}" + ("-local" if local_ctk else "")
105+
toolkit_args = [arg for args in installs for arg in args if arg.startswith("cuda-toolkit")]
106+
if local_ctk:
107+
assert toolkit_args == []
108+
else:
109+
minor = ".".join(cuda_version.split(".")[:2])
110+
assert f"cuda-toolkit=={minor}.*" in core_install
111+
assert f"cuda-toolkit[curand]=={minor}.*" in toolkit_args
100112
return bindings_install, core_install, str(bindings_wheel)
101113

102114
return run
@@ -109,24 +121,42 @@ def test_floor_uses_current_bindings_only_during_toolkit_bump(run_tests, local_c
109121
bindings, core, wheel = run_tests(local_ctk=local_ctk, floor=floor)
110122
expected = wheel + ("" if local_ctk else "[all]") if floor == "13.4.1" else "cuda-bindings==13.5.0"
111123
assert bindings == ["install", expected]
112-
assert "cuda-toolkit==13.0.*" in core
113124

114125

115126
@pytest.mark.parametrize("local_ctk", [True, False], ids=["local", "wheels"])
116127
@pytest.mark.agent_authored(model="gpt-6")
117-
def test_current_minor_requires_toolkit_wheels_only_for_wheel_jobs(run_tests, local_ctk):
128+
def test_current_minor_uses_local_or_wheel_test_dependencies(run_tests, local_ctk):
118129
bindings, core, wheel = run_tests(source="main", cuda_version="13.5.0", local_ctk=local_ctk)
119130
assert bindings == ["install", wheel + ("" if local_ctk else "[all]")]
120-
pins = [arg for arg in core if arg.startswith("cuda-toolkit==")]
121-
assert pins == ([] if local_ctk else ["cuda-toolkit==13.5.*"])
122131

123132

124133
@pytest.mark.parametrize(("source", "cuda_version"), [("floor", "12.6.3"), ("backport", "12.9.1")])
134+
@pytest.mark.parametrize("local_ctk", [True, False], ids=["local", "wheels"])
135+
@pytest.mark.agent_authored(model="gpt-6")
136+
def test_prior_major_keeps_its_floor_or_backport_bindings(run_tests, source, cuda_version, local_ctk):
137+
bindings, core, wheel = run_tests(source=source, cuda_version=cuda_version, local_ctk=local_ctk)
138+
expected = "cuda-bindings==12.9.8" if source == "floor" else wheel + ("" if local_ctk else "[all]")
139+
assert bindings == ["install", expected]
140+
141+
142+
@pytest.mark.parametrize("major", [12, 13])
125143
@pytest.mark.agent_authored(model="gpt-6")
126-
def test_prior_major_keeps_its_floor_or_backport_bindings(run_tests, source, cuda_version):
127-
bindings, core, wheel = run_tests(source=source, cuda_version=cuda_version, local_ctk=False)
128-
assert bindings == ["install", "cuda-bindings==12.9.8" if source == "floor" else wheel + "[all]"]
129-
assert f"cuda-toolkit=={'.'.join(cuda_version.split('.')[:2])}.*" in core
144+
def test_local_core_groups_keep_test_dependencies_without_toolkit_wheels(major):
145+
pyproject = TOOLS.parents[1] / "cuda_core" / "pyproject.toml"
146+
groups = tomllib.loads(pyproject.read_text(encoding="utf-8"))["dependency-groups"]
147+
148+
def requirements(name):
149+
for item in groups[name]:
150+
if isinstance(item, str):
151+
yield item
152+
else:
153+
yield from requirements(item["include-group"])
154+
155+
local = set(requirements(f"test-cu{major}-local"))
156+
assert set(requirements("test")) | set(requirements("ml-dtypes")) <= local
157+
assert any(requirement.partition(";")[0].strip() == f"cupy-cuda{major}x" for requirement in local)
158+
assert not any(requirement.startswith("cuda-toolkit") for requirement in local)
159+
assert set(requirements(f"test-cu{major}")) == local | {f"cuda-toolkit[cudart]=={major}.*"}
130160

131161

132162
@pytest.mark.parametrize("mode", ["nightly-pytorch", "nightly-numba-cuda", "nightly-numba-cuda-mlir"])

‎cuda_core/pyproject.toml‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,10 @@ test = [
9393
]
9494
ml-dtypes = ["ml-dtypes>=0.5.4"]
9595
test-ft = ["pytest-run-parallel==0.10.0"]
96-
test-cu12 = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda12x; python_version < '3.15'", "cuda-toolkit[cudart]==12.*"] # runtime headers needed by CuPy
97-
test-cu13 = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda13x; python_version < '3.15'", "cuda-toolkit[cudart]==13.*"] # runtime headers needed by CuPy
96+
test-cu12-local = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda12x; python_version < '3.15'"]
97+
test-cu13-local = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda13x; python_version < '3.15'"]
98+
test-cu12 = [ {include-group = "test-cu12-local" }, "cuda-toolkit[cudart]==12.*"] # runtime headers needed by CuPy
99+
test-cu13 = [ {include-group = "test-cu13-local" }, "cuda-toolkit[cudart]==13.*"] # runtime headers needed by CuPy
98100

99101
[tool.uv]
100102
conflicts = [
@@ -106,6 +108,10 @@ conflicts = [
106108
{ group = "test-cu12" },
107109
{ group = "test-cu13" },
108110
],
111+
[
112+
{ group = "test-cu12-local" },
113+
{ group = "test-cu13-local" },
114+
],
109115
]
110116

111117
[project.urls]

‎cuda_core/tests/example_tests/test_run_samples.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,63 @@ def test_cupy_cuda_requirement_accepts_conda_cupy_distribution(tmp_path: Path, i
462462
assert requested == ["cupy-cuda13x", "cupy"]
463463

464464

465+
@pytest.mark.parametrize("available", [True, False], ids=["local-library", "missing-library"])
466+
@pytest.mark.agent_authored(model="gpt-6")
467+
def test_unversioned_curand_requirement_accepts_native_library(
468+
tmp_path: Path, installed_distributions, monkeypatch: pytest.MonkeyPatch, available: bool
469+
) -> None:
470+
pathfinder = ModuleType("cuda.pathfinder")
471+
472+
class DynamicLibNotFoundError(RuntimeError):
473+
pass
474+
475+
def load_library(name: str) -> object:
476+
assert name == "curand"
477+
if not available:
478+
raise DynamicLibNotFoundError("cuRAND is not installed")
479+
return object()
480+
481+
pathfinder.DynamicLibNotFoundError = DynamicLibNotFoundError
482+
pathfinder.load_nvidia_dynamic_lib = load_library
483+
monkeypatch.setattr(run_samples._runner.importlib, "import_module", lambda _name: pathfinder)
484+
485+
missing = run_samples.missing_dependencies(_sample(tmp_path, "['nvidia-curand']"))
486+
487+
assert (missing == []) is available
488+
489+
490+
@pytest.mark.agent_authored(model="gpt-6")
491+
def test_native_curand_check_tolerates_missing_pathfinder(
492+
tmp_path: Path, installed_distributions, monkeypatch: pytest.MonkeyPatch
493+
) -> None:
494+
def import_module(name: str) -> ModuleType:
495+
raise ModuleNotFoundError(name=name)
496+
497+
monkeypatch.setattr(run_samples._runner.importlib, "import_module", import_module)
498+
499+
missing = run_samples.missing_dependencies(_sample(tmp_path, "['nvidia-curand']"))
500+
501+
assert len(missing) == 1
502+
assert "no provider distribution installed" in missing[0]
503+
504+
505+
@pytest.mark.parametrize(
506+
"requirement", ["nvidia-curand>=11", "nvidia-curand[extra]", "nvidia-curand @ https://example.com/curand.whl"]
507+
)
508+
@pytest.mark.agent_authored(model="gpt-6")
509+
def test_curand_constraints_require_a_provider_distribution(
510+
tmp_path: Path, installed_distributions, monkeypatch: pytest.MonkeyPatch, requirement: str
511+
) -> None:
512+
def unexpected_import(name: str) -> ModuleType:
513+
raise AssertionError("constrained requirements must use distribution metadata")
514+
515+
monkeypatch.setattr(run_samples._runner.importlib, "import_module", unexpected_import)
516+
517+
missing = run_samples.missing_dependencies(_sample(tmp_path, f"[{requirement!r}]"))
518+
519+
assert len(missing) == 1
520+
521+
465522
@pytest.mark.agent_authored(model="gpt-5")
466523
def test_environment_markers_control_dependency_checks(tmp_path: Path, installed_distributions) -> None:
467524
_, requested = installed_distributions

‎cuda_python_test_helpers/cuda_python_test_helpers/sample_runner.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,20 @@ def _distribution_mismatches(requirement: Requirement, dist: importlib.metadata.
335335
return mismatches
336336

337337

338+
def _native_curand_available() -> bool:
339+
try:
340+
pathfinder = importlib.import_module("cuda.pathfinder")
341+
except ModuleNotFoundError as exc:
342+
if exc.name in {"cuda", "cuda.pathfinder"}:
343+
return False
344+
raise
345+
try:
346+
pathfinder.load_nvidia_dynamic_lib("curand")
347+
except pathfinder.DynamicLibNotFoundError:
348+
return False
349+
return True
350+
351+
338352
def missing_dependencies(example: Path) -> list[str]:
339353
"""Return useful diagnostics for unmet PEP 723 dependency requirements.
340354
@@ -367,6 +381,13 @@ def missing_dependencies(example: Path) -> list[str]:
367381
break
368382
installed.append(f"{provider} {dist.version}: {', '.join(mismatches)}")
369383
else:
384+
# A local Toolkit can provide cuRAND without a Python distribution.
385+
if (
386+
canonicalize_name(requirement.name) == "nvidia-curand"
387+
and not (requirement.specifier or requirement.extras or requirement.url)
388+
and _native_curand_available()
389+
):
390+
continue
370391
if installed:
371392
detail = "; ".join(installed)
372393
else:

0 commit comments

Comments
 (0)