diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 2f8e9a47a02..8abb5ac48e7 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -248,14 +248,14 @@ jobs: fi - name: Display structure of downloaded cuda-python artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'published' }} + if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} run: | pwd ls -lah cuda_python*.whl cuda_pathfinder/ - name: Display structure of downloaded cuda.bindings artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE != 'published' }} + env.BINDINGS_SOURCE != 'floor' }} run: | pwd ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 1b9fc0ff9cb..16db1ab8d4c 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -228,14 +228,14 @@ jobs: fi - name: Display structure of downloaded cuda-python artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'published' }} + if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} run: | Get-Location Get-ChildItem cuda_python*.whl | Select-Object Mode, LastWriteTime, Length, FullName - name: Display structure of downloaded cuda.bindings artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE != 'published' }} + env.BINDINGS_SOURCE != 'floor' }} run: | Get-Location Get-ChildItem -Recurse -Force $env:CUDA_BINDINGS_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName diff --git a/.gitignore b/.gitignore index d2b0d4ffdde..a7e10ed1b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ cuda_bindings/cuda/bindings/utils/_get_handle.pyx # Version files from setuptools_scm _version.py +# Generated by cuda_core/build_hooks.py at build time (see cuda/core/__init__.py). +cuda_core/cuda/core/_build_info.py # Distribution / packaging .Python diff --git a/ci/tools/cuda_core_bindings_floor.py b/ci/tools/cuda_core_bindings_floor.py new file mode 100644 index 00000000000..1af202317de --- /dev/null +++ b/ci/tools/cuda_core_bindings_floor.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Print the cuda-bindings floor of a cuda-core wheel for one CUDA major. + + cuda_core_bindings_floor.py --wheel dist/cuda_core-*.whl --major 13 + -> 13.4.1 + +CI installs `cuda-bindings==` next to a freshly built cuda-core wheel to +test the oldest cuda-bindings that wheel supports (BINDINGS_SOURCE=floor in +ci/tools/env-vars). The floor is read from the wheel under test rather than +from the checkout, so a nightly job that tests a wheel built from another +commit reads that wheel's floor. + +The wheel carries the import-free module cuda/core/_bindings_floor.py, at top +level in a single-major build and under cuda/core/cu/ in the merged +wheel; this script reads the CUDA_BINDINGS_FLOOR literal out of it (without +running the module) and prints the entry for `major` as a dotted version. +""" + +from __future__ import annotations + +import argparse +import ast +import sys +import zipfile +from pathlib import Path + +MODULE = "_bindings_floor.py" + + +def floors_from_source(source: str) -> dict[int, tuple[int, int, int]]: + """The CUDA_BINDINGS_FLOOR literal of _bindings_floor.py, parsed without executing it.""" + for node in ast.parse(source, MODULE).body: + if isinstance(node, ast.AnnAssign): + targets = [node.target] + elif isinstance(node, ast.Assign): + targets = node.targets + else: + continue + if node.value is not None and any(isinstance(t, ast.Name) and t.id == "CUDA_BINDINGS_FLOOR" for t in targets): + return ast.literal_eval(node.value) + raise SystemExit(f"{MODULE} does not assign CUDA_BINDINGS_FLOOR") + + +def floor_from_source(source: str, major: int) -> str: + floors = floors_from_source(source) + if major not in floors: + raise SystemExit(f"CUDA {major} is not a supported major (floors: {sorted(floors)})") + return ".".join(str(part) for part in floors[major]) + + +def floor_from_wheel(wheel: Path, major: int) -> str: + with zipfile.ZipFile(wheel) as zf: + names = set(zf.namelist()) + for candidate in (f"cuda/core/cu{major}/{MODULE}", f"cuda/core/{MODULE}"): + if candidate in names: + return floor_from_source(zf.read(candidate).decode("utf-8"), major) + raise SystemExit(f"{wheel.name} contains no {MODULE}; is it a cuda-core wheel?") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--wheel", type=Path, required=True, help="the cuda-core wheel under test") + parser.add_argument("--major", type=int, required=True, help="CUDA major series (12 or 13)") + args = parser.parse_args(argv) + print(floor_from_wheel(args.wheel, args.major)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ci/tools/env-vars b/ci/tools/env-vars index 4155753a4ee..03d074b5a30 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -64,23 +64,40 @@ elif [[ "${1}" == "test" ]]; then # BINDINGS_SOURCE controls which cuda-bindings to install at test time: # main — use the just-built bindings wheel from this CI run # backport — fetch bindings from the prior (N-1) branch - # published — install from PyPI (cuda-bindings==${TEST_CUDA_MAJOR}.${TEST_CUDA_MINOR}.*) + # floor — install the oldest cuda-bindings the cuda-core wheel under test + # supports, from PyPI (its per-major floor; see + # cuda_core/cuda/core/_bindings_floor.py and ci/tools/run-tests). + # Selected when the test CTK minor differs from the one the wheel + # was built against, so those rows exercise new cuda-core + floor + # bindings + older CTK libraries, the skew cuda-core supports. + # (cuda-bindings older than the floor is unsupported and fails at + # import: https://github.com/NVIDIA/cuda-python/issues/2783.) # # SKIP_CUDA_BINDINGS_TEST / SKIP_CYTHON_TEST control which *tests* to run # (they do NOT affect installation — that's BINDINGS_SOURCE's job). BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})" + # The prior-major half of the cuda-core wheel is built against ci/versions.yml's + # prev_build toolkit (and the backport branch's bindings). + BUILD_PREV_CUDA_VER="$(sed -n '/prev_build:/,/version:/s/.*version: *"\([^"]*\)".*/\1/p' ci/versions.yml)" + BUILD_PREV_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_PREV_CUDA_VER})" if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then - # Major mismatch (e.g. build=13.x, test=12.x): use the backport branch. - BINDINGS_SOURCE=backport SKIP_CUDA_BINDINGS_TEST=1 SKIP_CYTHON_TEST=1 + if [[ ${BUILD_PREV_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then + # Prior major, minor mismatch (e.g. built against 12.9, test=12.6): floor + # bindings from PyPI with the older CTK libraries. + BINDINGS_SOURCE=floor + else + # Prior major, same minor (e.g. build=13.x, test=12.9): the backport branch. + BINDINGS_SOURCE=backport + fi elif [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then - # Same major, minor mismatch (e.g. build=13.2, test=13.0): use published - # bindings from PyPI to test the real-world backward-compat scenario. - BINDINGS_SOURCE=published + # Same major, minor mismatch (e.g. build=13.4, test=13.0): floor bindings + # from PyPI with the older CTK libraries. + BINDINGS_SOURCE=floor SKIP_CUDA_BINDINGS_TEST=1 SKIP_CYTHON_TEST=1 else diff --git a/ci/tools/run-tests b/ci/tools/run-tests index cfa7e9d6a7a..7a9d90955a4 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -74,10 +74,14 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then # Resolve bindings based on BINDINGS_SOURCE (set by env-vars): # main/backport → local wheel from artifacts dir - # published → install from PyPI by version + # floor → the oldest cuda-bindings the core wheel under test supports, + # read from that wheel, installed from PyPI BINDINGS_ARGS=() - if [[ "${BINDINGS_SOURCE}" == "published" ]]; then - BINDINGS_ARGS+=("cuda-bindings==${TEST_CUDA_MAJOR}.${TEST_CUDA_MINOR}.*") + if [[ "${BINDINGS_SOURCE}" == "floor" ]]; then + CORE_WHL_FOR_FLOOR=("${CUDA_CORE_ARTIFACTS_DIR}"/*.whl) + BINDINGS_FLOOR="$(python ci/tools/cuda_core_bindings_floor.py --wheel "${CORE_WHL_FOR_FLOOR[0]}" --major "${TEST_CUDA_MAJOR}")" + echo "cuda-bindings floor of ${CORE_WHL_FOR_FLOOR[0]##*/} for CUDA ${TEST_CUDA_MAJOR}: ${BINDINGS_FLOOR}" + BINDINGS_ARGS+=("cuda-bindings==${BINDINGS_FLOOR}") else BINDINGS_ARGS=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl) if [[ "${LOCAL_CTK}" != 1 ]]; then diff --git a/ci/tools/tests/test_cuda_core_bindings_floor.py b/ci/tools/tests/test_cuda_core_bindings_floor.py new file mode 100644 index 00000000000..a8e427020a9 --- /dev/null +++ b/ci/tools/tests/test_cuda_core_bindings_floor.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import importlib.util +import zipfile +from pathlib import Path + +import pytest + +TOOLS = Path(__file__).resolve().parent.parent +REPO = TOOLS.parent.parent +FLOOR_MODULE = REPO / "cuda_core" / "cuda" / "core" / "_bindings_floor.py" + + +def _load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +tool = _load("cuda_core_bindings_floor", TOOLS / "cuda_core_bindings_floor.py") +floor_module = _load("cuda_core_bindings_floor_module", FLOOR_MODULE) + + +def _expected(major): + return floor_module.format_version(floor_module.CUDA_BINDINGS_FLOOR[major]) + + +def _wheel(tmp_path, entries): + path = tmp_path / "cuda_core-1.3.0-cp312-cp312-linux_x86_64.whl" + with zipfile.ZipFile(path, "w") as zf: + for name in entries: + zf.writestr(name, FLOOR_MODULE.read_text(encoding="utf-8")) + return path + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +@pytest.mark.parametrize("major", [12, 13]) +def test_reads_the_merged_wheel_layout(tmp_path, major): + wheel = _wheel(tmp_path, ["cuda/core/cu12/_bindings_floor.py", "cuda/core/cu13/_bindings_floor.py"]) + assert tool.floor_from_wheel(wheel, major) == _expected(major) + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_reads_a_single_major_wheel(tmp_path): + wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"]) + assert tool.floor_from_wheel(wheel, 13) == _expected(13) + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_rejects_a_wheel_without_the_module(tmp_path): + wheel = _wheel(tmp_path, []) + with pytest.raises(SystemExit, match="contains no _bindings_floor.py"): + tool.floor_from_wheel(wheel, 13) + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_rejects_an_unsupported_major(tmp_path): + wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"]) + with pytest.raises(SystemExit, match="CUDA 11 is not a supported major"): + tool.floor_from_wheel(wheel, 11) + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_cli_prints_the_floor(tmp_path, capsys): + wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"]) + assert tool.main(["--wheel", str(wheel), "--major", "13"]) == 0 + assert capsys.readouterr().out.strip() == _expected(13) diff --git a/cuda_core/AGENTS.md b/cuda_core/AGENTS.md index 7196f95638f..115e6312619 100644 --- a/cuda_core/AGENTS.md +++ b/cuda_core/AGENTS.md @@ -91,10 +91,20 @@ and agents should flag violations. objects that are not meant to be shared (e.g., the thread-local `Device`) do not need such guards (see #2321). Reference-count integrity is guaranteed; cache value-identity/idempotency is not. -- **Entry points assume the GIL is held**: the helpers in `_cpp/rt/` - are called from Cython with the GIL held and do not re-acquire it. Driver and - destructor callbacks run at arbitrary times, so they take the GIL (`with gil`) - and probe for interpreter shutdown before touching Python objects. +- **Entry points work with or without the GIL**: the helpers in `_cpp/rt/` + are called from Cython both inside and outside `with nogil` blocks. They + never require the GIL, release it around driver calls, and never acquire it + while holding a C++ lock; the only paths that acquire it are the reporting + wrappers (`pw_*`, `report_*`) and the one-time driver function-table fill + (`ensure_fn_table()`, see `_cpp/rt/DESIGN.md`). Driver and destructor + callbacks run at arbitrary times, so they take the GIL (`with gil`) and probe + for interpreter shutdown before touching Python objects. +- **Driver calls go through the table**: C++ calls the driver with + `DRIVER_CALL(name, args...)`, whose pointers come from cuda-bindings' + resolved table, never from the Cython wrappers. A function the installed + driver may lack is gated in Cython on `cy_driver_version()` at the version + cuda-bindings requests it at (the number in `driver_api.hpp`); the C++ + never checks a pointer for null. - **Lock ordering -- release the GIL before entering the driver**: any CUDA work reachable from a host callback or a retained object's `__del__` must release the GIL before calling the driver, to avoid GIL/driver-lock deadlocks (see the diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 545523867d1..5e74448b045 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -9,6 +9,7 @@ import functools import glob +import importlib.util import os import re import sys @@ -65,6 +66,37 @@ def _import_get_cuda_path_or_home(): return cuda.pathfinder.get_cuda_path_or_home +def _import_cuda_bindings(): + """Import cuda.bindings, working around PEP 517 namespace shadowing. + + Same problem and same repair as _import_get_cuda_path_or_home() (see + https://github.com/NVIDIA/cuda-python/issues/1824): in an isolated build the + project's own ``cuda/`` directory is the whole ``cuda`` namespace, so the + cuda-bindings pip installed into the build environment is not importable + until its ``cuda/`` directory is added to the namespace path. Raises + ModuleNotFoundError when no cuda-bindings is installed at all. + (importlib.metadata is no alternative: pip's in-process hook runner forwards + ``find_distributions`` without the requested name, so it reports this + project's own metadata for any name.) + """ + try: + import cuda.bindings + except ModuleNotFoundError as exc: + if exc.name not in ("cuda", "cuda.bindings"): + raise + import cuda + + for p in sys.path: + sp_cuda = Path(p) / "cuda" + if (sp_cuda / "bindings").is_dir(): + cuda.__path__ = list(cuda.__path__) + [str(sp_cuda)] + break + else: + raise + import cuda.bindings + return cuda.bindings + + @functools.cache def _get_cuda_path() -> str: get_cuda_path_or_home = _import_get_cuda_path_or_home() @@ -75,6 +107,43 @@ def _get_cuda_path() -> str: return cuda_path +_PACKAGE_DIR = Path(__file__).parent / "cuda" / "core" + +# Generated at build time by _write_build_info(); read by cuda/core/__init__.py. +_BUILD_INFO_PATH = _PACKAGE_DIR / "_build_info.py" + + +@functools.cache +def _load_bindings_floor(): + """Load cuda/core/_bindings_floor.py, the floor's single source of truth. + + Loaded by file path: the package this backend builds is not importable + during its own build, and the module is deliberately import-free. + """ + path = _PACKAGE_DIR / "_bindings_floor.py" + spec = importlib.util.spec_from_file_location("_cuda_core_bindings_floor", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _read_cuda_h_version(cuda_path: str) -> int: + """The CUDA_VERSION macro (e.g. 13040 for 13.4) of the cuda.h under cuda_path.""" + cuda_h = os.path.join(cuda_path, "include", "cuda.h") + try: + with open(cuda_h, encoding="utf-8") as f: + for line in f: + m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) + if m: + return int(m.group(1)) + except OSError: + pass + raise RuntimeError( + f"Cannot read CUDA_VERSION from {cuda_h}. " + "Ensure CUDA_PATH or CUDA_HOME points to a valid CUDA installation with include/cuda.h." + ) + + @functools.cache def _determine_cuda_major_version() -> str: """Determine the CUDA major version for building cuda.core. @@ -88,7 +157,9 @@ def _determine_cuda_major_version() -> str: 2. CUDA_VERSION macro in cuda.h from CUDA_PATH or CUDA_HOME Since CUDA_PATH or CUDA_HOME is required for the build (to provide include - directories), the cuda.h header should always be available. + directories), the cuda.h header should always be available. The override + only short-circuits this detection; _check_build_configuration() still + reads the header and rejects one whose major disagrees. """ # Explicit override, e.g. in CI. cuda_major = os.environ.get("CUDA_CORE_BUILD_MAJOR") @@ -97,27 +168,108 @@ def _determine_cuda_major_version() -> str: return cuda_major # Derive from the CUDA headers (the authoritative source for what we compile against). - cuda_path = _get_cuda_path() - cuda_h = os.path.join(cuda_path, "include", "cuda.h") try: - with open(cuda_h, encoding="utf-8") as f: - for line in f: - m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) - if m: - v = int(m.group(1)) - # CUDA_VERSION is e.g. 12020 for 12.2. - cuda_major = str(v // 1000) - print("CUDA MAJOR VERSION:", cuda_major) - return cuda_major - except OSError: - pass + cuda_version = _read_cuda_h_version(_get_cuda_path()) + except RuntimeError as exc: + # CUDA_PATH or CUDA_HOME is required for the build, so we should not reach + # here in normal circumstances. Raise an error to make the issue clear. + raise RuntimeError( + "Cannot determine CUDA major version. " + "Set CUDA_CORE_BUILD_MAJOR environment variable, or ensure CUDA_PATH or CUDA_HOME " + "points to a valid CUDA installation with include/cuda.h." + ) from exc + # CUDA_VERSION is e.g. 12020 for 12.2. + cuda_major = str(cuda_version // 1000) + print("CUDA MAJOR VERSION:", cuda_major) + return cuda_major - # CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here - # in normal circumstances. Raise an error to make the issue clear. - raise RuntimeError( - "Cannot determine CUDA major version. " - "Set CUDA_CORE_BUILD_MAJOR environment variable, or ensure CUDA_PATH or CUDA_HOME " - "points to a valid CUDA installation with include/cuda.h." + +def _check_build_configuration(cuda_path: str, cuda_major: str) -> None: + """Reject build configurations cuda.core does not support, then record the build. + + cuda.core supports one configuration per CUDA major series: the installed + cuda-bindings is at least the series' floor (cuda/core/_bindings_floor.py) + and the cuda.h it compiles against has the same major.minor as that + cuda-bindings, which is the header cuda-bindings itself was generated from. + The pip build requirement (get_requires_for_build_*) states the floor, but + conda-forge, pixi and --no-build-isolation installs bypass it, so the check + lives here, where every build path passes. + + A too-old cuda-bindings used to surface late as an ImportError at module + init or as a feature that was silently compiled out; a mismatched header as + an unclear Cython error (see https://github.com/NVIDIA/cuda-python/issues/2783). + """ + floor = _load_bindings_floor() + major = int(cuda_major) + if major not in floor.CUDA_BINDINGS_FLOOR: + raise RuntimeError( + f"cuda.core does not support CUDA {major}; supported CUDA major versions: " + f"{', '.join(str(m) for m in floor.SUPPORTED_CUDA_MAJORS)}" + ) + requirement = floor.pip_requirement(major) + + try: + bindings_version = _import_cuda_bindings().__version__ + except ModuleNotFoundError as exc: + raise RuntimeError( + f"cuda.core requires cuda-bindings to build (install '{requirement}'). " + "Isolated builds install it automatically; other builds must provide it." + ) from exc + bindings = floor.release_triple(bindings_version) + if bindings is None: + raise RuntimeError( + f"Cannot parse the installed cuda-bindings version {bindings_version!r}. " + "A shallow git clone of cuda-bindings reports a bogus version; see CONTRIBUTING.md." + ) + if bindings[0] != major: + raise RuntimeError( + f"Building cuda.core for CUDA {major}, but the installed cuda-bindings is " + f"{bindings_version}. Install '{requirement}'." + ) + if bindings < floor.CUDA_BINDINGS_FLOOR[major]: + raise RuntimeError( + f"cuda.core requires cuda-bindings >= {floor.format_version(floor.CUDA_BINDINGS_FLOOR[major])} " + f"for CUDA {major}, but {bindings_version} is installed. Install '{requirement}'." + ) + + cuda_version = _read_cuda_h_version(cuda_path) + header = (cuda_version // 1000, cuda_version // 10 % 100) + if header != bindings[:2]: + raise RuntimeError( + f"cuda.h under {cuda_path} is CUDA {header[0]}.{header[1]}, but the installed cuda-bindings " + f"is {bindings_version}. cuda.core must be built against a cuda.h of the same " + "major.minor as its cuda-bindings (the header cuda-bindings was generated from). " + "Point CUDA_PATH or CUDA_HOME at a matching CUDA Toolkit, or install matching cuda-bindings." + ) + print(f"Build configuration: CUDA {header[0]}.{header[1]} headers, cuda-bindings {bindings_version}") + _write_build_info(major, cuda_version, floor.CUDA_BINDINGS_FLOOR[major], bindings_version) + + +def _build_define_macros(cuda_major: str) -> list: + """Preprocessor macros that carry the build decision into the C++ (see _cpp/rt/versions.hpp).""" + floor = _load_bindings_floor() + major = int(cuda_major) + return [ + ("CUDA_CORE_BUILD_MAJOR", str(major)), + ("CUDA_CORE_MIN_CUDA_VERSION", str(floor.cuda_version_of(floor.CUDA_BINDINGS_FLOOR[major]))), + ] + + +def _write_build_info(cuda_major: int, cuda_version: int, floor: tuple, bindings_version: str) -> None: + """Record what this build compiled against, for the import-time check. + + cuda/core/__init__.py reads this module before it selects the versioned + subpackage and refuses an installed cuda-bindings older than the floor or + older, by minor, than the header (see _bindings_floor.required_minimum). + Like _version.py, the file is generated, gitignored, and shipped. + """ + _BUILD_INFO_PATH.write_text( + "# Generated by build_hooks.py at build time. Do not edit or commit.\n" + f"CUDA_MAJOR = {cuda_major}\n" + f"CUDA_VERSION = {cuda_version} # the cuda.h this build compiled against\n" + f"CUDA_BINDINGS_FLOOR = {tuple(floor)!r}\n" + f"CUDA_BINDINGS_BUILD_VERSION = {bindings_version!r}\n", + encoding="utf-8", ) @@ -238,10 +390,10 @@ def _build_cuda_core(debug=False): # We need to add the directory containing the 'cuda' package so Cython can resolve # "from cuda.bindings cimport cydriver" try: - import cuda.bindings + cuda_bindings = _import_cuda_bindings() - bindings_path = Path(cuda.bindings.__file__).parent # .../cuda/bindings/ - print(f"Using cuda-bindings {cuda.bindings.__version__} from {bindings_path}", file=sys.stderr) + bindings_path = Path(cuda_bindings.__file__).parent # .../cuda/bindings/ + print(f"Using cuda-bindings {cuda_bindings.__version__} from {bindings_path}", file=sys.stderr) cuda_package_dir = bindings_path.parent.parent # .../cuda_bindings/ (contains cuda/) if str(cuda_package_dir) not in sys.path: sys.path.insert(0, str(cuda_package_dir)) @@ -288,6 +440,12 @@ def module_names(): # related to free-threading builds. extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1", "-DCYTHON_USE_SYS_MONITORING=0"] + # Deliberately after the cuda.bindings import above: this re-enters + # _get_cuda_path() and reads cuda.h, which must not run before the + # pathfinder import has repaired PEP 517 namespace shadowing. + cuda_major = _check_build_major() + _check_build_configuration(cuda_path, cuda_major) + depends = _extension_depends() ext_modules = tuple( Extension( @@ -299,6 +457,9 @@ def module_names(): "cuda/core/_cpp", ] + all_include_dirs, + # The C++ branches on the CUDA major series only; _cpp/rt/versions.hpp + # re-checks cuda.h against both macros (see _check_build_configuration). + define_macros=_build_define_macros(cuda_major), language="c++", extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, @@ -306,11 +467,6 @@ def module_names(): for mod in module_names() ) - # Deliberately after the cuda.bindings import above: this re-enters - # _get_cuda_path() and reads cuda.h, which must not run before the - # pathfinder import has repaired PEP 517 namespace shadowing. - cuda_major = _check_build_major() - nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2)) compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(cuda_major)} compiler_directives = {"embedsignature": True, "warn.deprecated.IF": False, "freethreading_compatible": True} @@ -441,8 +597,19 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): def _get_cuda_bindings_require(): - cuda_major = _determine_cuda_major_version() - return [f"cuda-bindings=={cuda_major}.*"] + """The cuda-bindings build requirement: the floor of the CUDA major being built. + + Honored by isolated builds only; _check_build_configuration() enforces the + same rule for every other build path. + """ + floor = _load_bindings_floor() + cuda_major = int(_determine_cuda_major_version()) + if cuda_major not in floor.CUDA_BINDINGS_FLOOR: + raise RuntimeError( + f"cuda.core does not support CUDA {cuda_major}; supported CUDA major versions: " + f"{', '.join(str(m) for m in floor.SUPPORTED_CUDA_MAJORS)}" + ) + return [floor.pip_requirement(cuda_major)] def get_requires_for_build_editable(config_settings=None): diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 5020f0f2a83..5c03c958611 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -6,13 +6,52 @@ def _import_versioned_module() -> None: + """Select the build for the installed cuda-bindings, after checking it is supported. + + The published wheel carries one build per CUDA major series, as the + subpackages ``cuda.core.cu12`` and ``cuda.core.cu13``; a conda or local + build carries one build at the top level. Each build records the CUDA + header it compiled against and its cuda-bindings floor in ``_build_info`` + (generated by build_hooks.py). The installed cuda-bindings must be of the + build's major and at least as new as the build's minimum + (see ``_bindings_floor.required_minimum``), or import fails here with an + actionable message instead of later with a missing C function or a + silently disabled feature. + """ import importlib - from cuda import bindings - - cuda_major = bindings.__version__.split(".")[0] - if cuda_major not in ("12", "13"): - raise ImportError("cuda.bindings 12.x or 13.x must be installed") + try: + from cuda import bindings + except ModuleNotFoundError as exc: + if exc.name in ("cuda", "cuda.bindings"): + raise ImportError("cuda.core requires cuda-bindings; install cuda-core[cu12] or cuda-core[cu13]") from None + raise + + def load_build_module(name: str, cuda_major: int): + # Prefer this major's build in the merged wheel; fall back to a plain build. + try: + return importlib.import_module(f".cu{cuda_major}.{name}", __package__) + except ModuleNotFoundError as exc: + if exc.name != f"{__package__}.cu{cuda_major}": + raise + return importlib.import_module(f".{name}", __package__) + + version_str = bindings.__version__ + # The major decides which build to consult; _bindings_floor validates everything else. + try: + cuda_major = int(version_str.split(".")[0]) + except ValueError: + cuda_major = -1 + if cuda_major not in (12, 13): + raise ImportError(f"cuda-bindings 12.x or 13.x must be installed (found {version_str})") + try: + floor = load_build_module("_bindings_floor", cuda_major) + info = load_build_module("_build_info", cuda_major) + except ModuleNotFoundError as exc: + raise ImportError( + f"this cuda.core installation has no build for CUDA {cuda_major} (installed cuda-bindings: {version_str})" + ) from exc + floor.check_installed_bindings(version_str, info.CUDA_MAJOR, info.CUDA_VERSION, __version__) subdir = f"cu{cuda_major}" try: diff --git a/cuda_core/cuda/core/_bindings_floor.py b/cuda_core/cuda/core/_bindings_floor.py new file mode 100644 index 00000000000..6e79f8392a1 --- /dev/null +++ b/cuda_core/cuda/core/_bindings_floor.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The cuda-bindings version floor: the single source of truth. + +cuda.core supports two CUDA major series at a time and requires, for each, a +minimum cuda-bindings version (the *floor*) at build time and at run time. The +floor is the newest cuda-bindings release of that series that the CI source +root can build, normally the release cuda.core's own wheels are built against. +See https://github.com/NVIDIA/cuda-python/issues/2783 and the support policy. + +This module is imported by the build backend (``build_hooks.py``, by file +path, because the package is not importable during its own build), by +``cuda/core/__init__.py`` at import time, and by tests that keep the static +pins in ``pyproject.toml`` and ``ci/versions.yml`` in step. It therefore uses +the standard library only and must not import anything from ``cuda``. + +Bumping a floor is a release-note item under "Breaking Changes". Bump it in the +same PR that first uses a cuda-bindings API newer than the old floor; the CI +rows that install the floor bindings fail otherwise. +""" + +from __future__ import annotations + +import re + +__all__ = [ + "CUDA_BINDINGS_FLOOR", + "SUPPORTED_CUDA_MAJORS", + "check_installed_bindings", + "cuda_version_of", + "format_version", + "pip_requirement", + "release_triple", + "required_minimum", +] + +# Minimum cuda-bindings release per CUDA major series, as a (major, minor, patch) +# triple. Keep in step with the `cu12`/`cu13` extras in pyproject.toml (tested). +CUDA_BINDINGS_FLOOR: dict[int, tuple[int, int, int]] = { + 12: (12, 9, 8), + 13: (13, 4, 1), +} + +SUPPORTED_CUDA_MAJORS = tuple(sorted(CUDA_BINDINGS_FLOOR)) + +_RELEASE_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)") + + +def release_triple(version: str) -> tuple[int, int, int] | None: + """The leading ``major.minor.patch`` of a version string, or None. + + Pre-release and development suffixes are ignored, so ``13.4.1``, + ``13.4.1a0`` and ``13.4.2.dev249+gabcdef`` yield (13, 4, 1), (13, 4, 1) + and (13, 4, 2). A string without three leading integers (for example the + ``0.1.dev1`` that setuptools-scm reports for a shallow clone) yields None. + """ + m = _RELEASE_RE.match(version.strip()) + if m is None: + return None + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + +def format_version(triple: tuple[int, ...]) -> str: + return ".".join(str(part) for part in triple) + + +def cuda_version_of(triple: tuple[int, int, int]) -> int: + """The ``CUDA_VERSION`` macro value (e.g. 13040) for a version triple's major.minor.""" + return triple[0] * 1000 + triple[1] * 10 + + +def pip_requirement(major: int) -> str: + """The pip requirement that pins cuda-bindings to the floor and the major.""" + return f"cuda-bindings>={format_version(CUDA_BINDINGS_FLOOR[major])},=={major}.*" + + +def required_minimum(cuda_major: int, header_cuda_version: int) -> tuple[int, int, int]: + """The minimum cuda-bindings a build accepts at run time. + + A build accepts the floor of its major series, and never a cuda-bindings + whose minor is older than the ``cuda.h`` the build compiled against: the + driver function-pointer keys the C++ layer looks up in cuda-bindings are + derived from that header's macros, so an older minor may lack them. + """ + floor = CUDA_BINDINGS_FLOOR[cuda_major] + header_minor = (header_cuda_version // 1000, header_cuda_version // 10 % 100, 0) + return max(floor, header_minor) + + +def check_installed_bindings( + installed_version: str, + build_cuda_major: int, + build_cuda_version: int, + core_version: str, +) -> tuple[int, int, int]: + """Validate the installed cuda-bindings against this build; return its triple. + + Raises ImportError with an actionable message when the installed + cuda-bindings is not a release of a supported major, is not the major + this build was compiled for, or is older than the build's minimum. + """ + installed = release_triple(installed_version) + if installed is None or installed[0] not in CUDA_BINDINGS_FLOOR: + majors = " or ".join(f"{m}.x" for m in SUPPORTED_CUDA_MAJORS) + raise ImportError(f"cuda-bindings {majors} must be installed (found {installed_version})") + major = installed[0] + if major != build_cuda_major: + raise ImportError( + f"this cuda.core {core_version} build is for CUDA {build_cuda_major}, but the installed " + f"cuda-bindings is {installed_version}. Install cuda-bindings {build_cuda_major}.x, " + f"or a cuda.core build for CUDA {major}." + ) + minimum = required_minimum(major, build_cuda_version) + if installed < minimum: + floor = format_version(minimum) + raise ImportError( + f"cuda.core {core_version} requires cuda-bindings >= {floor} for CUDA {major} " + f"(found {installed_version}). Upgrade with: pip install -U 'cuda-bindings>={floor},=={major}.*'" + ) + return installed diff --git a/cuda_core/cuda/core/_cpp/rt/DESIGN.md b/cuda_core/cuda/core/_cpp/rt/DESIGN.md index 0374d5a08be..ed0073a7392 100644 --- a/cuda_core/cuda/core/_cpp/rt/DESIGN.md +++ b/cuda_core/cuda/core/_cpp/rt/DESIGN.md @@ -165,45 +165,86 @@ functions, Cython generates calls through `_rt.so` at runtime. This ensures all static and thread-local state lives in a single shared library, avoiding the duplicate state problem. -## CUDA driver function pointers via cuda-bindings' `__pyx_capi__` +## CUDA driver function pointers from cuda-bindings -**Problem**: cuda.core cannot directly call CUDA driver functions because: +**Problem**: cuda.core cannot link against `libcuda.so` at build time, and it +must not load the driver itself: cuda-bindings owns driver loading and symbol +resolution (with `cuGetProcAddress`, which also selects the ABI variant and the +per-thread-default-stream variant). Until #2783, the C++ called cuda-bindings' +*Cython wrappers*, extracted from `cydriver.__pyx_capi__`. When the driver +lacked a function, a wrapper raised a Python exception that C++ never saw and +returned a sentinel `CUresult`; the exception surfaced later as `SystemError`. +And a wrapper could be absent when the installed cuda-bindings was older than +the build, so some pointers were optional and probed for null. -1. We don't want to link against `libcuda.so` at build time. -2. The driver symbols must be resolved dynamically through cuda-bindings. - -**Solution**: The C++ code declares extern function pointer variables: +**Solution**: `driver_api.hpp` lists every driver function the C++ calls, with +the CUDA version cuda-bindings requests it at: ```cpp -// driver_api.hpp -extern decltype(&cuStreamCreateWithPriority) p_cuStreamCreateWithPriority; -extern decltype(&cuMemPoolCreate) p_cuMemPoolCreate; -// ... etc -``` - -At module import time, `_rt.pyx` populates these pointers by -extracting them from `cuda.bindings.cydriver.__pyx_capi__`: - -```cython -import cuda.bindings.cydriver as cydriver - -cdef void* _get_driver_fn(str name): - capsule = cydriver.__pyx_capi__[name] - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - -p_cuStreamCreateWithPriority = _get_driver_fn("cuStreamCreateWithPriority") +#define CUDA_CORE_DRIVER_FUNCTIONS(X) \ + X(cuStreamCreateWithPriority, 5050) \ + X(cuGreenCtxStreamCreate, 12050) \ + ... ``` -The `__pyx_capi__` dictionary contains PyCapsules that Cython automatically -generates for each `cdef` function declared in a `.pxd` file. Each capsule's -name is the function's C signature; we query it with `PyCapsule_GetName()` -rather than hardcoding signatures. - -This approach: -- Avoids linking against `libcuda.so` at build time -- Works on CPU-only machines (capsule extraction succeeds; actual driver calls - will return errors like `CUDA_ERROR_NO_DEVICE`) -- Requires no custom capsule infrastructure—uses Cython's built-in mechanism +The list declares the `p_cuXxx` pointers and builds a table of +`{key, name, slot, introduced}` entries. The key is `"__"` plus the symbol +`cuda.h` maps the name to (`cuStreamDestroy` -> `"__cuStreamDestroy_v2"`), +which is how cuda-bindings names the slot in +`cuda.bindings._internal.driver._inspect_function_pointers()`. Because the key +follows the header's macros, the build requires the header's major.minor to +equal cuda-bindings' (see "Build-time version guards"). + +The table is filled lazily by `ensure_fn_table()` (`py_driver_fns.cpp`), the +first time a `DRIVER_CALL(name, args...)` finds its pointer null, so +`import cuda.core` never touches the driver. The fill acquires the GIL, calls +`_inspect_function_pointers()`, and copies every entry's address into its +`p_` pointer under a mutex that is never held across a Python call. It then +checks that every function introduced at or before the CUDA major series' +first release is present; a null one means the driver is older than the series +and the fill fails with that message. + +After the fill a pointer is either the driver's entry point or null because +the installed driver does not provide a newer function. Those functions are +gated on the driver version in Cython (`cy_driver_version()`), never by a null +check in C++. A `DRIVER_CALL` that still finds null after the fill is a gate +bug or a failed fill: it reports through `report_message()` and returns +`CUDA_ERROR_NOT_INITIALIZED` from a trampoline of the right signature, so it +never dereferences null and never throws, which makes it safe in `noexcept` +deleters. The `pw_` wrappers go through the same path. Owning handle +constructors whose deleter calls the driver but which do not call it +themselves (`create_graph_handle`, ...) call `ensure_fn_table()` so the fill +never happens in a deleter. NVRTC, NVVM and nvJitLink have one table each, +filled by `create_*_handle` after the library has loaded. + +Two rules follow. A `DRIVER_CALL` must not be made while a C++ lock is held, +because the fill acquires the GIL; resolve the table before the lock +(`ensure_fn_table(FnTable::driver)`) and use the raw pointer inside it, marked +`// raw:` (see `deviceptr_import_ipc`). And a driver function the Cython layer +gates must be gated at the version cuda-bindings requests it at (the number in +the table), not the version the driver first shipped it. + +`tests/test_rt_layout.py` checks the table against cuda-bindings' loader and +that no raw `p_` call exists outside the machinery and the marked lines. + +## Build-time version guards + +cuda.core supports one build configuration per CUDA major series: the `cuda.h` +it compiles against has the same major.minor as the cuda-bindings it is built +with, and that cuda-bindings is at or above the series' floor +(`cuda/core/_bindings_floor.py`). `build_hooks.py` enforces both before +compiling and defines `CUDA_CORE_BUILD_MAJOR` and `CUDA_CORE_MIN_CUDA_VERSION` +for the C++ compiler; `versions.hpp`, the first include of the tree, re-checks +`cuda.h` against them with `#error`. + +The C++ branches on `CUDA_CORE_BUILD_MAJOR` only, and only where the two major +series differ. Minor-version fences (`#if CUDA_VERSION >= 130x0`) are not +allowed: they compiled features out of source builds against an older header +while the run-time checks, which looked at the bindings and the driver, never +noticed (https://github.com/NVIDIA/cuda-python/issues/2783). Whether the +*driver* provides a function is decided by the driver-version gates in Cython, +never by the C++ layer. `tests/test_rt_layout.py` enforces that `versions.hpp` +is the only file under `_cpp/` that names `CUDA_VERSION`. ## Key Implementation Details @@ -233,8 +274,13 @@ Handle destructors may run from any thread. The implementation includes RAII gua - Handle Python finalization gracefully (avoid GIL operations during shutdown) - Ensure Python object manipulation happens with GIL held -The handle API functions are safe to call with or without the GIL held. They -will release the GIL (if necessary) before calling CUDA driver API functions. +The handle API functions may be called with or without the GIL held (Cython +calls most of them from `with nogil` blocks and some with the GIL held). They +never require it and never take a C++ lock while acquiring it. They release the +GIL (if necessary) before calling CUDA driver API functions. The only places +that acquire the GIL are the reporting paths (`pw_*`, `report_*`) and the +one-time function-table fill (`ensure_fn_table()`), neither of which may run +while a C++ lock is held. **The GIL is the outermost lock.** Code that holds a C++ lock (a registry's mutex, `ipc_import_mutex`, any `std::mutex`) must not acquire or reacquire the @@ -368,9 +414,10 @@ exactly one of these; none is ever dropped. ### `p_` versus `pw_` -A `p_` function pointer calls the driver and nothing else. Its `pw_` twin calls -the driver and, if the call fails, acquires the GIL and runs Python: the warning -filters, `showwarning`, or `sys.unraisablehook`. Any of those can be user code, +A `DRIVER_CALL` (a `p_` function pointer) calls the driver and nothing else, +once the table is filled. Its `pw_` twin calls the driver and, if the call +fails, acquires the GIL and runs Python: the warning filters, `showwarning`, or +`sys.unraisablehook`. Any of those can be user code, and user code can call back into cuda.core. This is the one place where the handle layer runs code it does not control, and it is the entry point through which a thread holding a C++ lock can deadlock (see "GIL Management"). diff --git a/cuda_core/cuda/core/_cpp/rt/context.cpp b/cuda_core/cuda/core/_cpp/rt/context.cpp index 89478484738..b0b6d162ab2 100644 --- a/cuda_core/cuda/core/_cpp/rt/context.cpp +++ b/cuda_core/cuda/core/_cpp/rt/context.cpp @@ -43,11 +43,11 @@ CUresult enter_context(const ContextHandle& h_context, CUcontext* previous, int* } GILReleaseGuard gil; - CUresult status = p_cuCtxGetCurrent(previous); + CUresult status = DRIVER_CALL(cuCtxGetCurrent, previous); if (status != CUDA_SUCCESS || *previous == target) { return status; } - status = p_cuCtxSetCurrent(target); + status = DRIVER_CALL(cuCtxSetCurrent, target); *changed = status == CUDA_SUCCESS; return status; } @@ -62,7 +62,7 @@ CUresult restore_context(CUcontext previous) noexcept { return fault; } GILReleaseGuard gil; - return p_cuCtxSetCurrent(previous); + return DRIVER_CALL(cuCtxSetCurrent, previous); } // Restore the previous context and preserve an earlier operation error. The // operation error, if any, is returned; otherwise the restoration status is. @@ -82,7 +82,7 @@ CUresult exit_context(CUcontext previous, int changed, CUresult operation_status CUresult context_synchronize(const ContextHandle& h_context) noexcept { GILReleaseGuard gil; return invoke_in_context(h_context, []() noexcept { - return p_cuCtxSynchronize(); + return DRIVER_FN(cuCtxSynchronize)(); }); } @@ -92,14 +92,14 @@ CUresult context_get_stream_priority_range(const ContextHandle& h_context, int* greatest_priority) noexcept { GILReleaseGuard gil; return invoke_in_context(h_context, [&]() noexcept { - return p_cuCtxGetStreamPriorityRange(least_priority, greatest_priority); + return DRIVER_CALL(cuCtxGetStreamPriorityRange, least_priority, greatest_priority); }); } // Query the device of the provided context. CUresult context_get_device(const ContextHandle& h_context, CUdevice* device) noexcept { return invoke_in_context(h_context, [&]() noexcept { - return p_cuCtxGetDevice(device); + return DRIVER_CALL(cuCtxGetDevice, device); }); } @@ -157,13 +157,8 @@ ContextHandle create_context_handle_from_green_ctx(const GreenCtxHandle& h_green if (!h_green_ctx) { return {}; } - if (!p_cuCtxFromGreenCtx) { - err = CUDA_ERROR_NOT_SUPPORTED; - return {}; - } - CUcontext ctx = nullptr; - if (CUDA_SUCCESS != (err = p_cuCtxFromGreenCtx(&ctx, as_cu(h_green_ctx)))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuCtxFromGreenCtx, &ctx, as_cu(h_green_ctx)))) { return {}; } @@ -180,18 +175,13 @@ GreenCtxHandle get_context_green_ctx(const ContextHandle& h) noexcept { GreenCtxHandle create_green_ctx_handle(CUdevResource* resources, unsigned int nbResources, CUdevice dev, unsigned int flags) { GILReleaseGuard gil; - if (!p_cuDevResourceGenerateDesc || !p_cuGreenCtxCreate || !p_cuGreenCtxDestroy) { - err = CUDA_ERROR_NOT_SUPPORTED; - return {}; - } - CUdevResourceDesc desc = nullptr; - if (CUDA_SUCCESS != (err = p_cuDevResourceGenerateDesc(&desc, resources, nbResources))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuDevResourceGenerateDesc, &desc, resources, nbResources))) { return {}; } CUgreenCtx green_ctx = nullptr; - if (CUDA_SUCCESS != (err = p_cuGreenCtxCreate(&green_ctx, desc, dev, flags))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuGreenCtxCreate, &green_ctx, desc, dev, flags))) { return {}; } @@ -228,7 +218,7 @@ ContextHandle get_primary_context(int device_id) { // Cache miss - acquire primary context from driver GILReleaseGuard gil; CUcontext ctx; - if (CUDA_SUCCESS != (err = p_cuDevicePrimaryCtxRetain(&ctx, device_id))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuDevicePrimaryCtxRetain, &ctx, device_id))) { return {}; } @@ -236,13 +226,12 @@ ContextHandle get_primary_context(int device_id) { new ContextBox{ctx, {}}, [device_id](const ContextBox* b) { context_registry.unregister_handle(b->resource); - // The driver function pointer targets a Cython __pyx_capi__ - // wrapper, which touches the Python runtime even though the - // underlying CUDA call does not. During interpreter shutdown, - // leave primary-context cleanup to process teardown. + // During interpreter shutdown, leave primary-context cleanup to + // process teardown (an unavailable table entry would need Python + // to report itself). if (Py_IsInitialized() && !py_is_finalizing()) { GILReleaseGuard gil; - p_cuDevicePrimaryCtxRelease(device_id); + DRIVER_CALL(cuDevicePrimaryCtxRelease, device_id); } delete b; } @@ -261,7 +250,7 @@ ContextHandle get_primary_context(int device_id) { ContextHandle get_current_context() { GILReleaseGuard gil; CUcontext ctx = nullptr; - if (CUDA_SUCCESS != (err = p_cuCtxGetCurrent(&ctx))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuCtxGetCurrent, &ctx))) { return {}; } if (!ctx) { diff --git a/cuda_core/cuda/core/_cpp/rt/context_scope.hpp b/cuda_core/cuda/core/_cpp/rt/context_scope.hpp index d1965d55a52..040a8eb9fa8 100644 --- a/cuda_core/cuda/core/_cpp/rt/context_scope.hpp +++ b/cuda_core/cuda/core/_cpp/rt/context_scope.hpp @@ -65,7 +65,7 @@ CUresult invoke_in_context_or_undo(const ContextHandle& h_context, Fn&& operatio bool undo_ok = true; if (undo_requires_target_context) { CUcontext current = nullptr; - undo_ok = p_cuCtxGetCurrent(¤t) == CUDA_SUCCESS + undo_ok = DRIVER_CALL(cuCtxGetCurrent, ¤t) == CUDA_SUCCESS && current == as_cu(h_context); } if (undo_ok) { diff --git a/cuda_core/cuda/core/_cpp/rt/driver_api.cpp b/cuda_core/cuda/core/_cpp/rt/driver_api.cpp index 860a29a3554..83b6b92530e 100644 --- a/cuda_core/cuda/core/_cpp/rt/driver_api.cpp +++ b/cuda_core/cuda/core/_cpp/rt/driver_api.cpp @@ -8,157 +8,57 @@ namespace cuda_core::rt { -// ============================================================================ -// CUDA driver function pointers -// -// These are populated by _rt.pyx at module import time using -// function pointers extracted from cuda.bindings.cydriver.__pyx_capi__. -// ============================================================================ - -decltype(&cuGetErrorName) p_cuGetErrorName = nullptr; -decltype(&cuGetErrorString) p_cuGetErrorString = nullptr; - -decltype(&cuDevicePrimaryCtxRetain) p_cuDevicePrimaryCtxRetain = nullptr; -decltype(&cuDevicePrimaryCtxRelease) p_cuDevicePrimaryCtxRelease = nullptr; -decltype(&cuCtxGetCurrent) p_cuCtxGetCurrent = nullptr; -decltype(&cuCtxSetCurrent) p_cuCtxSetCurrent = nullptr; -decltype(&cuCtxSynchronize) p_cuCtxSynchronize = nullptr; -decltype(&cuCtxGetStreamPriorityRange) p_cuCtxGetStreamPriorityRange = nullptr; -decltype(&cuCtxGetDevice) p_cuCtxGetDevice = nullptr; -decltype(&cuGraphNodeSetParams) p_cuGraphNodeSetParams = nullptr; -decltype(&cuGreenCtxCreate) p_cuGreenCtxCreate = nullptr; -decltype(&cuGreenCtxDestroy) p_cuGreenCtxDestroy = nullptr; -decltype(&cuCtxFromGreenCtx) p_cuCtxFromGreenCtx = nullptr; -decltype(&cuDevResourceGenerateDesc) p_cuDevResourceGenerateDesc = nullptr; - -decltype(&cuGreenCtxStreamCreate) p_cuGreenCtxStreamCreate = nullptr; - -decltype(&cuStreamCreateWithPriority) p_cuStreamCreateWithPriority = nullptr; -decltype(&cuStreamDestroy) p_cuStreamDestroy = nullptr; -decltype(&cuStreamGetCtx) p_cuStreamGetCtx = nullptr; - -decltype(&cuEventCreate) p_cuEventCreate = nullptr; -decltype(&cuEventDestroy) p_cuEventDestroy = nullptr; -decltype(&cuIpcOpenEventHandle) p_cuIpcOpenEventHandle = nullptr; - -decltype(&cuDeviceGetCount) p_cuDeviceGetCount = nullptr; - -decltype(&cuMemPoolSetAccess) p_cuMemPoolSetAccess = nullptr; -decltype(&cuMemPoolDestroy) p_cuMemPoolDestroy = nullptr; -decltype(&cuMemPoolCreate) p_cuMemPoolCreate = nullptr; -decltype(&cuDeviceGetMemPool) p_cuDeviceGetMemPool = nullptr; -decltype(&cuMemPoolImportFromShareableHandle) p_cuMemPoolImportFromShareableHandle = nullptr; - -decltype(&cuMemAllocFromPoolAsync) p_cuMemAllocFromPoolAsync = nullptr; -decltype(&cuMemAllocAsync) p_cuMemAllocAsync = nullptr; -decltype(&cuMemAlloc) p_cuMemAlloc = nullptr; -decltype(&cuMemAllocHost) p_cuMemAllocHost = nullptr; - -decltype(&cuMemFreeAsync) p_cuMemFreeAsync = nullptr; -decltype(&cuMemFree) p_cuMemFree = nullptr; -decltype(&cuMemFreeHost) p_cuMemFreeHost = nullptr; - -decltype(&cuMemPoolImportPointer) p_cuMemPoolImportPointer = nullptr; - -decltype(&cuLibraryLoadFromFile) p_cuLibraryLoadFromFile = nullptr; -decltype(&cuLibraryLoadData) p_cuLibraryLoadData = nullptr; -decltype(&cuLibraryUnload) p_cuLibraryUnload = nullptr; -decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel = nullptr; - -// Graph -decltype(&cuGraphDestroy) p_cuGraphDestroy = nullptr; -decltype(&cuGraphInstantiateWithParams) p_cuGraphInstantiateWithParams = nullptr; -decltype(&cuGraphExecUpdate) p_cuGraphExecUpdate = nullptr; -decltype(&cuGraphExecDestroy) p_cuGraphExecDestroy = nullptr; -decltype(&cuUserObjectCreate) p_cuUserObjectCreate = nullptr; -decltype(&cuUserObjectRelease) p_cuUserObjectRelease = nullptr; -decltype(&cuGraphRetainUserObject) p_cuGraphRetainUserObject = nullptr; -decltype(&cuGraphReleaseUserObject) p_cuGraphReleaseUserObject = nullptr; -decltype(&cuGraphNodeFindInClone) p_cuGraphNodeFindInClone = nullptr; -decltype(&cuGraphChildGraphNodeGetGraph) p_cuGraphChildGraphNodeGetGraph = nullptr; - -// Linker -decltype(&cuLinkDestroy) p_cuLinkDestroy = nullptr; - -// GL interop pointers -decltype(&cuGraphicsUnmapResources) p_cuGraphicsUnmapResources = nullptr; -decltype(&cuGraphicsUnregisterResource) p_cuGraphicsUnregisterResource = nullptr; +// The pointers. Null until ensure_fn_table() fills the table; see driver_api.hpp. +#define CUDA_CORE_DEFINE_DRIVER_FN(name, introduced) decltype(&name) p_##name = nullptr; +CUDA_CORE_DRIVER_FUNCTIONS(CUDA_CORE_DEFINE_DRIVER_FN) +#undef CUDA_CORE_DEFINE_DRIVER_FN -decltype(&cuArray3DCreate) p_cuArray3DCreate = nullptr; -decltype(&cuArrayDestroy) p_cuArrayDestroy = nullptr; -decltype(&cuMipmappedArrayCreate) p_cuMipmappedArrayCreate = nullptr; -decltype(&cuMipmappedArrayDestroy) p_cuMipmappedArrayDestroy = nullptr; -decltype(&cuMipmappedArrayGetLevel) p_cuMipmappedArrayGetLevel = nullptr; -decltype(&cuTexObjectCreate) p_cuTexObjectCreate = nullptr; -decltype(&cuTexObjectDestroy) p_cuTexObjectDestroy = nullptr; -decltype(&cuSurfObjectCreate) p_cuSurfObjectCreate = nullptr; -decltype(&cuSurfObjectDestroy) p_cuSurfObjectDestroy = nullptr; - -// SM resource split (13.1+ — may be null on older drivers/bindings) -#if CUDA_VERSION >= 13010 -decltype(&cuDevSmResourceSplit) p_cuDevSmResourceSplit = nullptr; -#else -void* p_cuDevSmResourceSplit = nullptr; -#endif - -// cuMemcpyWithAttributesAsync (13.2+ — may be null on older drivers/bindings) -#if CUDA_VERSION >= 13020 -decltype(&cuMemcpyWithAttributesAsync) p_cuMemcpyWithAttributesAsync = nullptr; -#else -void* p_cuMemcpyWithAttributesAsync = nullptr; -#endif - -// NVRTC function pointers decltype(&nvrtcDestroyProgram) p_nvrtcDestroyProgram = nullptr; - -// NVVM function pointers (may be null if NVVM is not available) NvvmDestroyProgramFn p_nvvmDestroyProgram = nullptr; - -// nvJitLink function pointers (may be null if nvJitLink is not available) NvJitLinkDestroyFn p_nvJitLinkDestroy = nullptr; -// ============================================================================ -// SM resource split wrapper -// ============================================================================ - -CUresult sm_resource_split(CUdevResource* result, unsigned int nbGroups, - const CUdevResource* input, CUdevResource* remainder, - unsigned int flags, void* groupParams) { -#if CUDA_VERSION >= 13010 - if (!p_cuDevSmResourceSplit) { - return CUDA_ERROR_NOT_SUPPORTED; - } - return p_cuDevSmResourceSplit( - result, nbGroups, input, remainder, flags, - static_cast(groupParams)); -#else - return CUDA_ERROR_NOT_SUPPORTED; -#endif -} - -bool has_sm_resource_split() noexcept { - return p_cuDevSmResourceSplit != nullptr; -} - -// ============================================================================ -// cuMemcpyWithAttributesAsync wrapper -// ============================================================================ - -CUresult memcpy_with_attributes_async(CUdeviceptr dst, CUdeviceptr src, size_t size, - void* attr, CUstream hStream) { -#if CUDA_VERSION >= 13020 - if (!p_cuMemcpyWithAttributesAsync) { - return CUDA_ERROR_NOT_SUPPORTED; +namespace { + +#define CUDA_CORE_STR(x) #x +#define CUDA_CORE_XSTR(x) CUDA_CORE_STR(x) + +// "__" + the symbol cuda.h maps the public name to (macro-expanded), which is +// how cuda-bindings keys its table; #name is the public name, unexpanded. +#define CUDA_CORE_DRIVER_FN_ENTRY(name, introduced) \ + {"__" CUDA_CORE_XSTR(name), #name, reinterpret_cast(&p_##name), introduced}, + +const FnEntry driver_entries[] = {CUDA_CORE_DRIVER_FUNCTIONS(CUDA_CORE_DRIVER_FN_ENTRY)}; +#undef CUDA_CORE_DRIVER_FN_ENTRY + +const FnEntry nvrtc_entries[] = { + {"__nvrtcDestroyProgram", "nvrtcDestroyProgram", reinterpret_cast(&p_nvrtcDestroyProgram), 0}, +}; +const FnEntry nvvm_entries[] = { + {"__nvvmDestroyProgram", "nvvmDestroyProgram", reinterpret_cast(&p_nvvmDestroyProgram), 0}, +}; +const FnEntry nvjitlink_entries[] = { + {"__nvJitLinkDestroy", "nvJitLinkDestroy", reinterpret_cast(&p_nvJitLinkDestroy), 0}, +}; + +} // namespace + +const FnEntry* fn_table_entries(FnTable table, std::size_t* count) noexcept { + switch (table) { + case FnTable::driver: + *count = sizeof(driver_entries) / sizeof(driver_entries[0]); + return driver_entries; + case FnTable::nvrtc: + *count = sizeof(nvrtc_entries) / sizeof(nvrtc_entries[0]); + return nvrtc_entries; + case FnTable::nvvm: + *count = sizeof(nvvm_entries) / sizeof(nvvm_entries[0]); + return nvvm_entries; + case FnTable::nvjitlink: + *count = sizeof(nvjitlink_entries) / sizeof(nvjitlink_entries[0]); + return nvjitlink_entries; } - return p_cuMemcpyWithAttributesAsync( - dst, src, size, static_cast(attr), hStream); -#else - return CUDA_ERROR_NOT_SUPPORTED; -#endif -} - -bool has_memcpy_with_attributes_async() noexcept { - return p_cuMemcpyWithAttributesAsync != nullptr; + *count = 0; + return nullptr; } } // namespace cuda_core::rt diff --git a/cuda_core/cuda/core/_cpp/rt/driver_api.hpp b/cuda_core/cuda/core/_cpp/rt/driver_api.hpp index 87ed3bd7906..b2955816647 100644 --- a/cuda_core/cuda/core/_cpp/rt/driver_api.hpp +++ b/cuda_core/cuda/core/_cpp/rt/driver_api.hpp @@ -12,179 +12,229 @@ namespace cuda_core::rt { // ============================================================================ -// CUDA driver function pointers +// Driver and compiler-library function pointers // -// These are populated by _rt.pyx at module import time using -// function pointers extracted from cuda.bindings.cydriver.__pyx_capi__. -// ============================================================================ - -extern decltype(&cuGetErrorName) p_cuGetErrorName; -extern decltype(&cuGetErrorString) p_cuGetErrorString; - -extern decltype(&cuDevicePrimaryCtxRetain) p_cuDevicePrimaryCtxRetain; -extern decltype(&cuDevicePrimaryCtxRelease) p_cuDevicePrimaryCtxRelease; -extern decltype(&cuCtxGetCurrent) p_cuCtxGetCurrent; -extern decltype(&cuCtxSetCurrent) p_cuCtxSetCurrent; -extern decltype(&cuCtxSynchronize) p_cuCtxSynchronize; -extern decltype(&cuCtxGetStreamPriorityRange) p_cuCtxGetStreamPriorityRange; -extern decltype(&cuCtxGetDevice) p_cuCtxGetDevice; -extern decltype(&cuGraphNodeSetParams) p_cuGraphNodeSetParams; -extern decltype(&cuGreenCtxCreate) p_cuGreenCtxCreate; -extern decltype(&cuGreenCtxDestroy) p_cuGreenCtxDestroy; -extern decltype(&cuCtxFromGreenCtx) p_cuCtxFromGreenCtx; -extern decltype(&cuDevResourceGenerateDesc) p_cuDevResourceGenerateDesc; - -extern decltype(&cuGreenCtxStreamCreate) p_cuGreenCtxStreamCreate; - -extern decltype(&cuStreamCreateWithPriority) p_cuStreamCreateWithPriority; -extern decltype(&cuStreamDestroy) p_cuStreamDestroy; -extern decltype(&cuStreamGetCtx) p_cuStreamGetCtx; - -extern decltype(&cuEventCreate) p_cuEventCreate; -extern decltype(&cuEventDestroy) p_cuEventDestroy; -extern decltype(&cuIpcOpenEventHandle) p_cuIpcOpenEventHandle; - -extern decltype(&cuDeviceGetCount) p_cuDeviceGetCount; - -extern decltype(&cuMemPoolSetAccess) p_cuMemPoolSetAccess; -extern decltype(&cuMemPoolDestroy) p_cuMemPoolDestroy; -extern decltype(&cuMemPoolCreate) p_cuMemPoolCreate; -extern decltype(&cuDeviceGetMemPool) p_cuDeviceGetMemPool; -extern decltype(&cuMemPoolImportFromShareableHandle) p_cuMemPoolImportFromShareableHandle; - -extern decltype(&cuMemAllocFromPoolAsync) p_cuMemAllocFromPoolAsync; -extern decltype(&cuMemAllocAsync) p_cuMemAllocAsync; -extern decltype(&cuMemAlloc) p_cuMemAlloc; -extern decltype(&cuMemAllocHost) p_cuMemAllocHost; - -extern decltype(&cuMemFreeAsync) p_cuMemFreeAsync; -extern decltype(&cuMemFree) p_cuMemFree; -extern decltype(&cuMemFreeHost) p_cuMemFreeHost; - -extern decltype(&cuMemPoolImportPointer) p_cuMemPoolImportPointer; - -// Library -extern decltype(&cuLibraryLoadFromFile) p_cuLibraryLoadFromFile; -extern decltype(&cuLibraryLoadData) p_cuLibraryLoadData; -extern decltype(&cuLibraryUnload) p_cuLibraryUnload; -extern decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel; - -// Graph -extern decltype(&cuGraphDestroy) p_cuGraphDestroy; -extern decltype(&cuGraphInstantiateWithParams) p_cuGraphInstantiateWithParams; -extern decltype(&cuGraphExecUpdate) p_cuGraphExecUpdate; -extern decltype(&cuGraphExecDestroy) p_cuGraphExecDestroy; -extern decltype(&cuUserObjectCreate) p_cuUserObjectCreate; -extern decltype(&cuUserObjectRelease) p_cuUserObjectRelease; -extern decltype(&cuGraphRetainUserObject) p_cuGraphRetainUserObject; -extern decltype(&cuGraphReleaseUserObject) p_cuGraphReleaseUserObject; -extern decltype(&cuGraphNodeFindInClone) p_cuGraphNodeFindInClone; -extern decltype(&cuGraphChildGraphNodeGetGraph) p_cuGraphChildGraphNodeGetGraph; - -// Linker -extern decltype(&cuLinkDestroy) p_cuLinkDestroy; - -// Graphics interop -extern decltype(&cuGraphicsUnmapResources) p_cuGraphicsUnmapResources; -extern decltype(&cuGraphicsUnregisterResource) p_cuGraphicsUnregisterResource; - -// Texture / surface / array (PR #467) -extern decltype(&cuArray3DCreate) p_cuArray3DCreate; -extern decltype(&cuArrayDestroy) p_cuArrayDestroy; -extern decltype(&cuMipmappedArrayCreate) p_cuMipmappedArrayCreate; -extern decltype(&cuMipmappedArrayDestroy) p_cuMipmappedArrayDestroy; -extern decltype(&cuMipmappedArrayGetLevel) p_cuMipmappedArrayGetLevel; -extern decltype(&cuTexObjectCreate) p_cuTexObjectCreate; -extern decltype(&cuTexObjectDestroy) p_cuTexObjectDestroy; -extern decltype(&cuSurfObjectCreate) p_cuSurfObjectCreate; -extern decltype(&cuSurfObjectDestroy) p_cuSurfObjectDestroy; - -// SM resource split (13.1+ — may be null on older drivers/bindings) -#if CUDA_VERSION >= 13010 -extern decltype(&cuDevSmResourceSplit) p_cuDevSmResourceSplit; -#else -// cuDevSmResourceSplit doesn't exist in CUDA < 13.1 headers, so use a -// void* placeholder. The pointer is always null when built against 12.x. -extern void* p_cuDevSmResourceSplit; -#endif - -// cuMemcpyWithAttributesAsync (13.2+ — may be null on older drivers/bindings) -#if CUDA_VERSION >= 13020 -extern decltype(&cuMemcpyWithAttributesAsync) p_cuMemcpyWithAttributesAsync; -#else -// cuMemcpyWithAttributesAsync doesn't exist in CUDA < 13.2 headers, so use a -// void* placeholder. The pointer is always null when built against older CUDA. -extern void* p_cuMemcpyWithAttributesAsync; -#endif - -// ============================================================================ -// NVRTC function pointers +// The C++ under _cpp/rt/ calls the CUDA driver through the p_cuXxx pointers +// below. They hold the driver's own entry points, taken from the table that +// cuda-bindings builds when it loads the driver (cuGetProcAddress for each +// symbol, choosing the ABI variant and the per-thread-default-stream variant) +// and exposes as cuda.bindings._internal.driver._inspect_function_pointers(). +// cuda.core never loads the driver or resolves a symbol itself, and it never +// calls cuda-bindings' Cython wrappers from C++: those raise a Python +// exception when the driver lacks a function, which C++ cannot see +// (https://github.com/NVIDIA/cuda-python/issues/2783). // -// These are populated by _rt.pyx at module import time using -// function pointers extracted from cuda.bindings.cynvrtc.__pyx_capi__. -// ============================================================================ - -extern decltype(&nvrtcDestroyProgram) p_nvrtcDestroyProgram; - -// ============================================================================ -// NVVM function pointers +// The table is filled lazily, the first time a DRIVER_CALL finds its pointer +// null, so that `import cuda.core` never touches the driver. The fill acquires +// the GIL and runs Python (see py_driver_fns.cpp), so a DRIVER_CALL must not +// be made while a C++ lock is held; call ensure_fn_table() before taking the +// lock and use the raw pointer inside it (see deviceptr_import_ipc). // -// These are populated by _rt.pyx at module import time using -// function pointers extracted from cuda.bindings.cynvvm.__pyx_capi__. -// Note: May be null if NVVM is not available at runtime. +// After the fill a pointer is either the driver's entry point or null because +// the installed driver does not provide that function. Functions introduced +// at or before the first release of the CUDA major series being built are +// present in every driver cuda.core supports; the fill checks them and +// rejects an older driver. A newer function can legitimately be null, and the +// Cython layer gates its use on the driver version (cy_driver_version()), so +// the C++ never checks a pointer for null before a call. A DRIVER_CALL that +// still finds null after the fill is therefore a gate bug (or a failed fill); +// it reports an internal error and returns CUDA_ERROR_NOT_INITIALIZED from a +// trampoline of the right signature instead of dereferencing null. It never +// throws, so it is safe in noexcept deleters and cleanup paths. // ============================================================================ -// Function pointer type for nvvmDestroyProgram (avoids nvvm.h dependency) -// Signature: nvvmResult nvvmDestroyProgram(nvvmProgram *prog) +// Each X(name, introduced) names a driver function cuda.core calls and the CUDA +// version cuda-bindings requests it at (cuGetProcAddress's cudaVersion; the +// ABI's introduction). tests/test_rt_layout.py checks the list against +// cuda-bindings' loader. `name` is the public name; cuda.h may map it to a +// versioned symbol (cuStreamDestroy -> cuStreamDestroy_v2), and the table key +// follows that mapping, so the header cuda.core compiles against must be the one +// cuda-bindings was generated from (enforced by build_hooks.py). +#define CUDA_CORE_DRIVER_FUNCTIONS(X) \ + /* Error formatting */ \ + X(cuGetErrorName, 6000) \ + X(cuGetErrorString, 6000) \ + /* Context */ \ + X(cuDevicePrimaryCtxRetain, 7000) \ + X(cuDevicePrimaryCtxRelease, 11000) \ + X(cuCtxGetCurrent, 4000) \ + X(cuCtxSetCurrent, 4000) \ + X(cuCtxSynchronize, 2000) \ + X(cuCtxGetStreamPriorityRange, 5050) \ + X(cuCtxGetDevice, 2000) \ + X(cuGraphNodeSetParams, 12020) \ + X(cuGreenCtxCreate, 12040) \ + X(cuGreenCtxDestroy, 12040) \ + X(cuCtxFromGreenCtx, 12040) \ + X(cuDevResourceGenerateDesc, 12040) \ + X(cuGreenCtxStreamCreate, 12050) \ + /* Stream */ \ + X(cuStreamCreateWithPriority, 5050) \ + X(cuStreamDestroy, 4000) \ + X(cuStreamGetCtx, 9020) \ + /* Event */ \ + X(cuEventCreate, 2000) \ + X(cuEventDestroy, 4000) \ + X(cuIpcOpenEventHandle, 4010) \ + /* Device */ \ + X(cuDeviceGetCount, 2000) \ + /* Memory pool */ \ + X(cuMemPoolSetAccess, 11020) \ + X(cuMemPoolDestroy, 11020) \ + X(cuMemPoolCreate, 11020) \ + X(cuDeviceGetMemPool, 11020) \ + X(cuMemPoolImportFromShareableHandle, 11020) \ + /* Memory allocation */ \ + X(cuMemAllocFromPoolAsync, 11020) \ + X(cuMemAllocAsync, 11020) \ + X(cuMemAlloc, 3020) \ + X(cuMemAllocHost, 3020) \ + X(cuMemFreeAsync, 11020) \ + X(cuMemFree, 3020) \ + X(cuMemFreeHost, 2000) \ + /* IPC */ \ + X(cuMemPoolImportPointer, 11020) \ + /* Library */ \ + X(cuLibraryLoadFromFile, 12000) \ + X(cuLibraryLoadData, 12000) \ + X(cuLibraryUnload, 12000) \ + X(cuLibraryGetKernel, 12000) \ + /* Graph */ \ + X(cuGraphDestroy, 10000) \ + X(cuGraphInstantiateWithParams, 12000) \ + X(cuGraphExecUpdate, 12000) \ + X(cuGraphExecDestroy, 10000) \ + X(cuUserObjectCreate, 11030) \ + X(cuUserObjectRelease, 11030) \ + X(cuGraphRetainUserObject, 11030) \ + X(cuGraphReleaseUserObject, 11030) \ + X(cuGraphNodeFindInClone, 10000) \ + X(cuGraphChildGraphNodeGetGraph, 10000) \ + /* Linker */ \ + X(cuLinkDestroy, 5050) \ + /* Graphics interop */ \ + X(cuGraphicsUnmapResources, 7000) \ + X(cuGraphicsUnregisterResource, 3000) \ + /* Texture / surface / array (PR #467) */ \ + X(cuArray3DCreate, 3020) \ + X(cuArrayDestroy, 2000) \ + X(cuMipmappedArrayCreate, 5000) \ + X(cuMipmappedArrayDestroy, 5000) \ + X(cuMipmappedArrayGetLevel, 5000) \ + X(cuTexObjectCreate, 5000) \ + X(cuTexObjectDestroy, 5000) \ + X(cuSurfObjectCreate, 5000) \ + X(cuSurfObjectDestroy, 5000) + +#define CUDA_CORE_DECLARE_DRIVER_FN(name, introduced) extern decltype(&name) p_##name; +CUDA_CORE_DRIVER_FUNCTIONS(CUDA_CORE_DECLARE_DRIVER_FN) +#undef CUDA_CORE_DECLARE_DRIVER_FN + +// Compiler-library entry points, one table per library so that filling one +// does not load the others (NVVM and nvJitLink are optional at run time). +// Types are spelled out where the library header is not included; they match +// `nvvmResult nvvmDestroyProgram(nvvmProgram*)` and +// `nvJitLinkResult nvJitLinkDestroy(nvJitLinkHandle*)` as int-sized enums. +extern decltype(&nvrtcDestroyProgram) p_nvrtcDestroyProgram; using NvvmDestroyProgramFn = int (*)(nvvmProgram*); extern NvvmDestroyProgramFn p_nvvmDestroyProgram; - -// ============================================================================ -// nvJitLink function pointers -// -// These are populated by _rt.pyx at module import time using -// function pointers extracted from cuda.bindings.cynvjitlink.__pyx_capi__. -// Note: May be null if nvJitLink is not available at runtime. -// ============================================================================ - -// Function pointer type for nvJitLinkDestroy (avoids nvJitLink.h dependency) -// Signature: nvJitLinkResult nvJitLinkDestroy(nvJitLinkHandle *handle) using NvJitLinkDestroyFn = int (*)(nvJitLink_t*); extern NvJitLinkDestroyFn p_nvJitLinkDestroy; // ============================================================================ -// SM resource split wrapper (13.1+) -// -// Calls through p_cuDevSmResourceSplit if available, otherwise returns -// CUDA_ERROR_NOT_SUPPORTED. This avoids a direct Cython cimport of the -// cydriver cdef function, which would fail at module init on cuda-bindings -// < 13.1 (see https://github.com/NVIDIA/cuda-python/issues/2063). -// ============================================================================ - -// groupParams is void* so the Cython declaration doesn't reference -// CU_DEV_SM_RESOURCE_GROUP_PARAMS (absent from cuda-bindings 13.0 .pxd). -CUresult sm_resource_split(CUdevResource* result, unsigned int nbGroups, - const CUdevResource* input, CUdevResource* remainder, - unsigned int flags, void* groupParams); - -// Returns true if the cuDevSmResourceSplit function pointer is available. -bool has_sm_resource_split() noexcept; - -// ============================================================================ -// cuMemcpyWithAttributesAsync wrapper (13.2+) -// -// Calls through p_cuMemcpyWithAttributesAsync if available, otherwise returns -// CUDA_ERROR_NOT_SUPPORTED. This avoids a direct Cython cimport of the -// cydriver cdef function, which would fail at module init on cuda-bindings -// < 13.2 (see https://github.com/NVIDIA/cuda-python/issues/2063). +// Function tables // ============================================================================ -// attr is void* so the Cython declaration doesn't reference CUmemcpyAttributes -// (absent from cuda-bindings built against CUDA < 12.8). The C++ side casts it. -CUresult memcpy_with_attributes_async(CUdeviceptr dst, CUdeviceptr src, size_t size, - void* attr, CUstream hStream); - -// Returns true if the cuMemcpyWithAttributesAsync function pointer is available. -bool has_memcpy_with_attributes_async() noexcept; - +enum class FnTable { driver, nvrtc, nvvm, nvjitlink }; + +struct FnEntry { + const char* key; // cuda-bindings' name for the slot, e.g. "__cuStreamDestroy_v2" + const char* name; // public name, for messages, e.g. "cuStreamDestroy" + void** slot; // the p_ pointer, as storage + int introduced; // CUDA version cuda-bindings requests the symbol at (0 = n/a) +}; + +// The entries of a table. Implemented in driver_api.cpp. +const FnEntry* fn_table_entries(FnTable table, std::size_t* count) noexcept; + +// Whether a table has been filled (acquire: a true result orders the slots). +bool fn_table_ready(FnTable table) noexcept; + +// Fill a table from cuda-bindings if it is not ready. Acquires the GIL (never +// call with a C++ lock held), imports cuda.bindings._internal., calls +// _inspect_function_pointers(), and stores every entry's pointer. Returns +// false, records the reason (fn_table_error) and reports it through +// report_message() when cuda-bindings cannot load the library, a key is +// missing (the installed cuda-bindings does not match the header this build +// compiled against), or a baseline driver function is null (the driver is +// older than the CUDA major series supports). Never leaves a Python error set. +// Implemented in py_driver_fns.cpp. +bool ensure_fn_table(FnTable table) noexcept; + +// The reason the last fill of `table` failed, or nullptr. Implemented in py_driver_fns.cpp. +const char* fn_table_error(FnTable table) noexcept; + +// Report, once per table, that `name` was called while unavailable: a gate +// bug, or a failed fill (whose reason is included). Implemented in py_driver_fns.cpp. +void report_unavailable_fn(FnTable table, const char* name) noexcept; + +namespace detail { + +// The status a trampoline returns in place of an unavailable function. +template +struct UnavailableStatus; +template <> +struct UnavailableStatus { + static constexpr CUresult value = CUDA_ERROR_NOT_INITIALIZED; +}; +template <> +struct UnavailableStatus { + static constexpr nvrtcResult value = NVRTC_ERROR_INTERNAL_ERROR; +}; +template <> +struct UnavailableStatus { + static constexpr int value = -1; +}; + +// A function of the same signature as an unavailable entry point, so that a +// call site never dereferences null. Its status flows to the caller's normal +// error handling; the cause has already been reported. +template +struct Unavailable; +template +struct Unavailable { + static R call(A...) noexcept { return UnavailableStatus::value; } +}; + +// The resolved pointer, filling the table on first use; the trampoline when +// the function is unavailable after the fill. Never throws. +template +inline F fn_or_unavailable(F& slot, FnTable table, const char* name) noexcept { + if (!fn_table_ready(table)) { + ensure_fn_table(table); + } + if (F fn = slot) { + return fn; + } + report_unavailable_fn(table, name); + return &Unavailable::call; +} + +} // namespace detail } // namespace cuda_core::rt + +// A driver function's table entry, resolved on first use. DRIVER_CALL(name, args...) +// calls it; use these for every driver call in the C++ layer except under a C++ +// lock (see the header comment; such a call is marked `// raw:`). Each macro +// pastes its own parameter: passing `name` through another macro would let +// cuda.h's versioning macros rewrite it (cuMemFree -> cuMemFree_v2) first. +#define DRIVER_FN(name) \ + (::cuda_core::rt::detail::fn_or_unavailable(::cuda_core::rt::p_##name, ::cuda_core::rt::FnTable::driver, #name)) +#define DRIVER_CALL(name, ...) \ + (::cuda_core::rt::detail::fn_or_unavailable(::cuda_core::rt::p_##name, ::cuda_core::rt::FnTable::driver, #name)(__VA_ARGS__)) +#define NVRTC_CALL(name, ...) \ + (::cuda_core::rt::detail::fn_or_unavailable(::cuda_core::rt::p_##name, ::cuda_core::rt::FnTable::nvrtc, #name)(__VA_ARGS__)) +#define NVVM_CALL(name, ...) \ + (::cuda_core::rt::detail::fn_or_unavailable(::cuda_core::rt::p_##name, ::cuda_core::rt::FnTable::nvvm, #name)(__VA_ARGS__)) +#define NVJITLINK_CALL(name, ...) \ + (::cuda_core::rt::detail::fn_or_unavailable(::cuda_core::rt::p_##name, ::cuda_core::rt::FnTable::nvjitlink, #name)(__VA_ARGS__)) diff --git a/cuda_core/cuda/core/_cpp/rt/error.cpp b/cuda_core/cuda/core/_cpp/rt/error.cpp index 38ceb20f240..2f9f06f285c 100644 --- a/cuda_core/cuda/core/_cpp/rt/error.cpp +++ b/cuda_core/cuda/core/_cpp/rt/error.cpp @@ -39,8 +39,8 @@ void format_cuda_error(char* buffer, size_t size, const char* operation, CUresul const char* error_name = nullptr; const char* error_description = nullptr; bool decoded = p_cuGetErrorName && p_cuGetErrorString - && p_cuGetErrorName(status, &error_name) == CUDA_SUCCESS - && p_cuGetErrorString(status, &error_description) == CUDA_SUCCESS; + && DRIVER_CALL(cuGetErrorName, status, &error_name) == CUDA_SUCCESS + && DRIVER_CALL(cuGetErrorString, status, &error_description) == CUDA_SUCCESS; const char* outcome = detail ? detail : "failed"; if (decoded) { std::snprintf(buffer, size, "%s %s: %s: %s", operation, outcome, error_name, error_description); @@ -100,13 +100,13 @@ namespace detail { void note_context_not_restored(CUcontext previous, CUresult operation_status, CUresult restore_status) noexcept { CUcontext current = nullptr; - if (p_cuCtxGetCurrent(¤t) != CUDA_SUCCESS) { + if (DRIVER_CALL(cuCtxGetCurrent, ¤t) != CUDA_SUCCESS) { current = nullptr; } char cause[128] = {0}; if (operation_status != CUDA_SUCCESS) { const char* error_name = nullptr; - if (p_cuGetErrorName && p_cuGetErrorName(restore_status, &error_name) == CUDA_SUCCESS) { + if (DRIVER_CALL(cuGetErrorName, restore_status, &error_name) == CUDA_SUCCESS) { std::snprintf(cause, sizeof(cause), " after this failure (cuCtxSetCurrent: %s)", error_name); } else { std::snprintf(cause, sizeof(cause), " after this failure (cuCtxSetCurrent: CUDA error %d)", diff --git a/cuda_core/cuda/core/_cpp/rt/event.cpp b/cuda_core/cuda/core/_cpp/rt/event.cpp index 7514f8b8e41..73b6e600a0e 100644 --- a/cuda_core/cuda/core/_cpp/rt/event.cpp +++ b/cuda_core/cuda/core/_cpp/rt/event.cpp @@ -69,7 +69,7 @@ EventHandle create_event_handle(const ContextHandle& h_ctx, unsigned int flags, CUevent event = nullptr; err = invoke_in_context_or_undo( h_ctx, - [&]() noexcept { return p_cuEventCreate(&event, flags); }, + [&]() noexcept { return DRIVER_CALL(cuEventCreate, &event, flags); }, [&]() noexcept { pw_cuEventDestroy(event); }, /*undo_requires_target_context=*/false); if (err != CUDA_SUCCESS) { @@ -97,7 +97,7 @@ EventHandle create_event_handle_for_stream(CUstream stream, unsigned int flags) CUcontext ctx = nullptr; { GILReleaseGuard gil; - err = p_cuStreamGetCtx(stream, &ctx); + err = DRIVER_CALL(cuStreamGetCtx, stream, &ctx); } if (err != CUDA_SUCCESS) { return {}; @@ -121,7 +121,7 @@ EventHandle create_event_handle_ipc(const CUipcEventHandle& ipc_handle, bool is_blocking_sync) { GILReleaseGuard gil; CUevent event; - if (CUDA_SUCCESS != (err = p_cuIpcOpenEventHandle(&event, ipc_handle))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuIpcOpenEventHandle, &event, ipc_handle))) { return {}; } diff --git a/cuda_core/cuda/core/_cpp/rt/graph.cpp b/cuda_core/cuda/core/_cpp/rt/graph.cpp index 4f6105dfdaa..d10633db7de 100644 --- a/cuda_core/cuda/core/_cpp/rt/graph.cpp +++ b/cuda_core/cuda/core/_cpp/rt/graph.cpp @@ -32,9 +32,6 @@ CUresult graph_node_set_params(CUgraphNode node, CUgraphNodeParams* params, const ContextHandle& h_context, CUresult* restore_status) noexcept { *restore_status = CUDA_SUCCESS; - if (!p_cuGraphNodeSetParams) { - return CUDA_ERROR_NOT_SUPPORTED; - } CUcontext previous = nullptr; int changed = 0; CUresult status = enter_context(h_context, &previous, &changed); @@ -43,7 +40,7 @@ CUresult graph_node_set_params(CUgraphNode node, CUgraphNodeParams* params, } { GILReleaseGuard gil; - status = p_cuGraphNodeSetParams(node, params); + status = DRIVER_CALL(cuGraphNodeSetParams, node, params); } if (!changed) { return status; @@ -149,15 +146,11 @@ CUresult rekey_attachments( if (!cloned_graph) { return CUDA_ERROR_INVALID_VALUE; } - if (!p_cuGraphNodeFindInClone) { - return CUDA_ERROR_NOT_SUPPORTED; - } - GraphAttachmentMap remapped; while (!attachments.empty()) { auto attachment = attachments.extract(attachments.begin()); CUgraphNode cloned_node = nullptr; - CUresult status = p_cuGraphNodeFindInClone( + CUresult status = DRIVER_CALL(cuGraphNodeFindInClone, &cloned_node, attachment.key(), cloned_graph); if (status != CUDA_SUCCESS) { return status; @@ -210,22 +203,18 @@ void stage_graph_metadata( // must be populated before entry. The caller must release the GIL. CUresult rekey_graph_metadata( StagedGraphMetadataList& staged) { - if (!p_cuGraphNodeFindInClone || !p_cuGraphChildGraphNodeGetGraph) { - return CUDA_ERROR_NOT_SUPPORTED; - } - CUresult status; for (size_t i = 0; i < staged.size(); ++i) { const GraphBox& source = *staged[i].source; GraphBox& clone = *staged[i].clone; if (i != 0) { CUgraphNode cloned_owner = nullptr; - status = p_cuGraphNodeFindInClone( + status = DRIVER_CALL(cuGraphNodeFindInClone, &cloned_owner, source.owner_node, clone.parent->resource); if (status == CUDA_SUCCESS) { - status = p_cuGraphChildGraphNodeGetGraph( + status = DRIVER_CALL(cuGraphChildGraphNodeGetGraph, cloned_owner, &clone.resource); } if (status != CUDA_SUCCESS) { @@ -307,6 +296,7 @@ struct PreparedChildGraphUpdateState { }; GraphHandle create_graph_handle(CUgraph graph) { + ensure_fn_table(FnTable::driver); // the deleter calls the driver; resolve before it can run if (!graph) { return {}; } @@ -432,9 +422,9 @@ CUresult graph_commit_child_graph_update( CUresult status = CUDA_ERROR_NOT_SUPPORTED; CUgraph cloned_root = nullptr; - if (p_cuGraphChildGraphNodeGetGraph) { + { GILReleaseGuard gil; - status = p_cuGraphChildGraphNodeGetGraph( + status = DRIVER_CALL(cuGraphChildGraphNodeGetGraph, state.owner_node, &cloned_root); if (status == CUDA_SUCCESS) { state.staged.front().clone->resource = cloned_root; @@ -504,19 +494,10 @@ CUresult graph_prepare_attachment( if (!box->resource) { return CUDA_ERROR_INVALID_VALUE; } - if (!p_cuGraphReleaseUserObject) { - return CUDA_ERROR_NOT_SUPPORTED; - } - PreparedAttachment prepared( new PreparedAttachmentState(h_graph), PreparedAttachmentDeleter{rollback_prepared_attachment}); if (owner0 || owner1) { - if (!p_cuUserObjectCreate || !p_cuUserObjectRelease || - !p_cuGraphRetainUserObject) { - return CUDA_ERROR_NOT_SUPPORTED; - } - ensure_deferred_cleanup_ready(); prepared->replacement = new NodeAttachment( std::move(owner0), std::move(owner1)); @@ -538,7 +519,7 @@ CUresult graph_prepare_attachment( CUresult status; { GILReleaseGuard gil; - status = p_cuUserObjectCreate( + status = DRIVER_CALL(cuUserObjectCreate, &object, cleanup_item, reinterpret_cast(enqueue_cleanup), 1, CU_USER_OBJECT_NO_DESTRUCTOR_SYNC); @@ -549,7 +530,7 @@ CUresult graph_prepare_attachment( return status; } prepared->replacement->object = object; - status = p_cuGraphRetainUserObject( + status = DRIVER_CALL(cuGraphRetainUserObject, box->resource, object, 1, CU_GRAPH_USER_OBJECT_MOVE); if (status != CUDA_SUCCESS) { prepared->replacement_entry.mapped() = nullptr; @@ -613,7 +594,7 @@ CUresult graph_commit_attachment( return CUDA_SUCCESS; } GILReleaseGuard gil; - return p_cuGraphReleaseUserObject( + return DRIVER_CALL(cuGraphReleaseUserObject, box->resource, previous->object, 1); } diff --git a/cuda_core/cuda/core/_cpp/rt/graph_exec.cpp b/cuda_core/cuda/core/_cpp/rt/graph_exec.cpp index dbf09cf77e0..4927274fb79 100644 --- a/cuda_core/cuda/core/_cpp/rt/graph_exec.cpp +++ b/cuda_core/cuda/core/_cpp/rt/graph_exec.cpp @@ -90,7 +90,7 @@ struct ExecAttachmentStaging { const GraphHandle source = std::move(h_source); accumulator = nullptr; GILReleaseGuard gil; - return p_cuGraphReleaseUserObject(*source, object, 1); + return DRIVER_CALL(cuGraphReleaseUserObject, *source, object, 1); } }; @@ -98,11 +98,6 @@ struct ExecAttachmentStaging { // instantiation or whole-graph update propagates a reference into the exec. CUresult stage_exec_attachments( const GraphHandle& h_source, ExecAttachmentStaging* out_staging) { - if (!p_cuUserObjectCreate || !p_cuUserObjectRelease || - !p_cuGraphRetainUserObject || !p_cuGraphReleaseUserObject) { - return CUDA_ERROR_NOT_SUPPORTED; - } - ensure_deferred_cleanup_ready(); auto* accumulator = new ExecAttachments; @@ -110,7 +105,7 @@ CUresult stage_exec_attachments( CUresult status; { GILReleaseGuard gil; - status = p_cuUserObjectCreate( + status = DRIVER_CALL(cuUserObjectCreate, &object, static_cast(accumulator), reinterpret_cast(enqueue_cleanup), @@ -121,7 +116,7 @@ CUresult stage_exec_attachments( return status; } accumulator->object = object; - status = p_cuGraphRetainUserObject( + status = DRIVER_CALL(cuGraphRetainUserObject, *h_source, object, 1, CU_GRAPH_USER_OBJECT_MOVE); if (status != CUDA_SUCCESS) { // Dropping the last reference retires the accumulator. @@ -174,11 +169,6 @@ GraphExecHandle create_graph_exec_handle( err = CUDA_ERROR_INVALID_VALUE; return {}; } - if (!p_cuGraphInstantiateWithParams) { - err = CUDA_ERROR_NOT_SUPPORTED; - return {}; - } - ExecAttachmentStaging staging; if (CUDA_SUCCESS != (err = stage_exec_attachments(h_source, &staging))) { return {}; @@ -187,7 +177,7 @@ GraphExecHandle create_graph_exec_handle( CUgraphExec graph_exec = nullptr; { GILReleaseGuard gil; - err = p_cuGraphInstantiateWithParams(&graph_exec, *h_source, params); + err = DRIVER_CALL(cuGraphInstantiateWithParams, &graph_exec, *h_source, params); } if (err != CUDA_SUCCESS) { return {}; @@ -218,10 +208,6 @@ CUresult graph_exec_update( if (!h_exec || !h_source || !*h_source || !result_info) { return CUDA_ERROR_INVALID_VALUE; } - if (!p_cuGraphExecUpdate) { - return CUDA_ERROR_NOT_SUPPORTED; - } - GraphExecBox* box = get_exec_box(h_exec); if (!box->resource) { return CUDA_ERROR_INVALID_VALUE; @@ -235,7 +221,7 @@ CUresult graph_exec_update( { GILReleaseGuard gil; - status = p_cuGraphExecUpdate(box->resource, *h_source, result_info); + status = DRIVER_CALL(cuGraphExecUpdate, box->resource, *h_source, result_info); } if (status != CUDA_SUCCESS) { return status; diff --git a/cuda_core/cuda/core/_cpp/rt/internal.hpp b/cuda_core/cuda/core/_cpp/rt/internal.hpp index 46b5a77f379..6b22acefc74 100644 --- a/cuda_core/cuda/core/_cpp/rt/internal.hpp +++ b/cuda_core/cuda/core/_cpp/rt/internal.hpp @@ -72,18 +72,21 @@ bool make_deallocation_stream(const StreamHandle& h, DeallocationStream& out) no // one while holding a C++ lock; the GIL must be the outermost lock. Where a // lock must stay held, call the p_ pointer, keep the status, and report after // the lock is released (see deviceptr_import_ipc and DESIGN.md). -template +template class WarnOnFailure { public: explicit WarnOnFailure(const char* operation) noexcept : operation_(operation) {} // The first argument is the resource being released; it is named in the // report so that independent failures are not collapsed by the warning - // registry (see format_operation). + // registry (see format_operation). The call goes through the function + // table like DRIVER_CALL: an unavailable entry is reported and yields an + // error status, never a null dereference. template auto operator()(First&& first, Rest&&... rest) const noexcept { const unsigned long long handle = handle_bits(first); - auto status = Function(std::forward(first), std::forward(rest)...); + auto status = detail::fn_or_unavailable(Function, Table, operation_)( + std::forward(first), std::forward(rest)...); report(status, handle); return status; } @@ -129,9 +132,9 @@ const WarnOnFailure pw_cuGraphicsUnregisterResou const WarnOnFailure pw_cuLinkDestroy{"cuLinkDestroy"}; const WarnOnFailure pw_cuUserObjectRelease{"cuUserObjectRelease"}; const WarnOnFailure pw_cuGraphReleaseUserObject{"cuGraphReleaseUserObject"}; -const WarnOnFailure pw_nvrtcDestroyProgram{"nvrtcDestroyProgram"}; -const WarnOnFailure pw_nvvmDestroyProgram{"nvvmDestroyProgram"}; -const WarnOnFailure pw_nvJitLinkDestroy{"nvJitLinkDestroy"}; +const WarnOnFailure pw_nvrtcDestroyProgram{"nvrtcDestroyProgram"}; +const WarnOnFailure pw_nvvmDestroyProgram{"nvvmDestroyProgram"}; +const WarnOnFailure pw_nvJitLinkDestroy{"nvJitLinkDestroy"}; // Intrusive base for payloads transferred out of CUDA's callback. struct DeferredCleanupItem { diff --git a/cuda_core/cuda/core/_cpp/rt/memory.cpp b/cuda_core/cuda/core/_cpp/rt/memory.cpp index 68a22c835bc..9ad1b64bccb 100644 --- a/cuda_core/cuda/core/_cpp/rt/memory.cpp +++ b/cuda_core/cuda/core/_cpp/rt/memory.cpp @@ -43,7 +43,7 @@ struct MemoryPoolBox { static void clear_mempool_peer_access(CUmemoryPool pool, int owner_device) noexcept { try { int device_count = 0; - if (p_cuDeviceGetCount(&device_count) != CUDA_SUCCESS || device_count <= 0) { + if (DRIVER_CALL(cuDeviceGetCount, &device_count) != CUDA_SUCCESS || device_count <= 0) { return; } @@ -55,7 +55,7 @@ static void clear_mempool_peer_access(CUmemoryPool pool, int owner_device) noexc continue; } revoke.location.id = i; - p_cuMemPoolSetAccess(pool, &revoke, 1); // Best effort + DRIVER_CALL(cuMemPoolSetAccess, pool, &revoke, 1); // Best effort } } catch (...) { // Swallow exceptions - this is best-effort cleanup in destructor context @@ -80,7 +80,7 @@ static MemoryPoolHandle wrap_mempool_owned(CUmemoryPool pool, int owner_device) MemoryPoolHandle create_mempool_handle(const CUmemPoolProps& props) { GILReleaseGuard gil; CUmemoryPool pool; - if (CUDA_SUCCESS != (err = p_cuMemPoolCreate(&pool, &props))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemPoolCreate, &pool, &props))) { return {}; } int owner_device = props.location.type == CU_MEM_LOCATION_TYPE_DEVICE ? props.location.id : -1; @@ -95,7 +95,7 @@ MemoryPoolHandle create_mempool_handle_ref(CUmemoryPool pool) { MemoryPoolHandle get_device_mempool(int device_id) { GILReleaseGuard gil; CUmemoryPool pool; - if (CUDA_SUCCESS != (err = p_cuDeviceGetMemPool(&pool, device_id))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuDeviceGetMemPool, &pool, device_id))) { return {}; } return create_mempool_handle_ref(pool); @@ -105,7 +105,7 @@ MemoryPoolHandle create_mempool_handle_ipc(int fd, CUmemAllocationHandleType han GILReleaseGuard gil; CUmemoryPool pool; auto handle_ptr = reinterpret_cast(static_cast(fd)); - if (CUDA_SUCCESS != (err = p_cuMemPoolImportFromShareableHandle(&pool, handle_ptr, handle_type, 0))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemPoolImportFromShareableHandle, &pool, handle_ptr, handle_type, 0))) { return {}; } return wrap_mempool_owned(pool, -1); @@ -158,7 +158,7 @@ CUresult set_deallocation_stream(const DevicePtrHandle& h, const StreamHandle& h DevicePtrHandle deviceptr_alloc_from_pool(size_t size, const MemoryPoolHandle& h_pool, const StreamHandle& h_stream) { GILReleaseGuard gil; CUdeviceptr ptr; - if (CUDA_SUCCESS != (err = p_cuMemAllocFromPoolAsync(&ptr, size, *h_pool, as_cu(h_stream)))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemAllocFromPoolAsync, &ptr, size, *h_pool, as_cu(h_stream)))) { return {}; } @@ -176,7 +176,7 @@ DevicePtrHandle deviceptr_alloc_from_pool(size_t size, const MemoryPoolHandle& h cleanup_in_context( deallocation_context(stream), "cuMemFreeAsync", handle_bits(b->resource), [&]() noexcept { - return p_cuMemFreeAsync( + return DRIVER_CALL(cuMemFreeAsync, b->resource, as_cu(stream.h_stream)); }); delete b; @@ -188,7 +188,7 @@ DevicePtrHandle deviceptr_alloc_from_pool(size_t size, const MemoryPoolHandle& h DevicePtrHandle deviceptr_alloc_async(size_t size, const StreamHandle& h_stream) { GILReleaseGuard gil; CUdeviceptr ptr; - if (CUDA_SUCCESS != (err = p_cuMemAllocAsync(&ptr, size, as_cu(h_stream)))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemAllocAsync, &ptr, size, as_cu(h_stream)))) { return {}; } @@ -206,7 +206,7 @@ DevicePtrHandle deviceptr_alloc_async(size_t size, const StreamHandle& h_stream) cleanup_in_context( deallocation_context(stream), "cuMemFreeAsync", handle_bits(b->resource), [&]() noexcept { - return p_cuMemFreeAsync( + return DRIVER_CALL(cuMemFreeAsync, b->resource, as_cu(stream.h_stream)); }); delete b; @@ -221,7 +221,7 @@ CUresult deviceptr_alloc_raw(CUdeviceptr* ptr, size_t size, GILReleaseGuard gil; return invoke_in_context_or_undo( h_context, - [&]() noexcept { return p_cuMemAlloc(ptr, size); }, + [&]() noexcept { return DRIVER_CALL(cuMemAlloc, ptr, size); }, [&]() noexcept { pw_cuMemFree(*ptr); }, /*undo_requires_target_context=*/false); } @@ -229,7 +229,7 @@ CUresult deviceptr_alloc_raw(CUdeviceptr* ptr, size_t size, DevicePtrHandle deviceptr_alloc_host(size_t size) { GILReleaseGuard gil; void* ptr; - if (CUDA_SUCCESS != (err = p_cuMemAllocHost(&ptr, size))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemAllocHost, &ptr, size))) { return {}; } @@ -291,7 +291,7 @@ DevicePtrHandle deviceptr_create_mapped_graphics( cleanup_in_context( deallocation_context(stream), "cuGraphicsUnmapResources", handle_bits(resource), [&]() noexcept { - return p_cuGraphicsUnmapResources( + return DRIVER_CALL(cuGraphicsUnmapResources, 1, &resource, as_cu(stream.h_stream)); }); delete b; @@ -404,6 +404,10 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* auto data = const_cast( reinterpret_cast(export_data)); + // Resolve the table before any lock is taken: a fill acquires the GIL, and + // nothing under ipc_import_mutex may (#2840). The raw p_ calls below rely on it. + ensure_fn_table(FnTable::driver); + if (use_ipc_ptr_cache()) { ExportDataKey key; std::memcpy(&key.data, data, sizeof(key.data)); @@ -425,7 +429,7 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* } CUdeviceptr ptr; - if (CUDA_SUCCESS != (err = p_cuMemPoolImportPointer(&ptr, *h_pool, data))) { + if (CUDA_SUCCESS != (err = p_cuMemPoolImportPointer(&ptr, *h_pool, data))) { // raw: under ipc_import_mutex return {}; } @@ -449,7 +453,7 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* cleanup_in_context( h_dealloc, "cuMemFreeAsync", handle_bits(b->resource), [&]() noexcept { - return p_cuMemFreeAsync(b->resource, as_cu(stream.h_stream)); + return p_cuMemFreeAsync(b->resource, as_cu(stream.h_stream)); // raw: under ipc_import_mutex }, [&]() noexcept { lock.unlock(); }); delete b; @@ -462,7 +466,7 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* // No deallocation stream could be recorded: discard the import with // the raw call (a pw_ report would acquire the GIL under the mutex). - discard_status = p_cuMemFreeAsync(ptr, as_cu(h_stream)); + discard_status = p_cuMemFreeAsync(ptr, as_cu(h_stream)); // raw: under ipc_import_mutex discarded = ptr; } if (discard_status != CUDA_SUCCESS) { @@ -477,7 +481,7 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* } else { GILReleaseGuard gil; CUdeviceptr ptr; - if (CUDA_SUCCESS != (err = p_cuMemPoolImportPointer(&ptr, *h_pool, data))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMemPoolImportPointer, &ptr, *h_pool, data))) { return {}; } @@ -495,7 +499,7 @@ DevicePtrHandle deviceptr_import_ipc(const MemoryPoolHandle& h_pool, const void* cleanup_in_context( deallocation_context(stream), "cuMemFreeAsync", handle_bits(b->resource), [&]() noexcept { - return p_cuMemFreeAsync( + return DRIVER_CALL(cuMemFreeAsync, b->resource, as_cu(stream.h_stream)); }); delete b; diff --git a/cuda_core/cuda/core/_cpp/rt/program.cpp b/cuda_core/cuda/core/_cpp/rt/program.cpp index 74c6455264a..2a41f8fb125 100644 --- a/cuda_core/cuda/core/_cpp/rt/program.cpp +++ b/cuda_core/cuda/core/_cpp/rt/program.cpp @@ -28,7 +28,7 @@ struct LibraryBox { LibraryHandle create_library_handle_from_file(const char* path) { GILReleaseGuard gil; CUlibrary library; - if (CUDA_SUCCESS != (err = p_cuLibraryLoadFromFile(&library, path, nullptr, nullptr, 0, nullptr, nullptr, 0))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuLibraryLoadFromFile, &library, path, nullptr, nullptr, 0, nullptr, nullptr, 0))) { return {}; } @@ -47,7 +47,7 @@ LibraryHandle create_library_handle_from_file(const char* path) { LibraryHandle create_library_handle_from_data(const void* data) { GILReleaseGuard gil; CUlibrary library; - if (CUDA_SUCCESS != (err = p_cuLibraryLoadData(&library, data, nullptr, nullptr, 0, nullptr, nullptr, 0))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuLibraryLoadData, &library, data, nullptr, nullptr, 0, nullptr, nullptr, 0))) { return {}; } @@ -92,7 +92,7 @@ static HandleRegistry kernel_registry; KernelHandle create_kernel_handle(const LibraryHandle& h_library, const char* name) { GILReleaseGuard gil; CUkernel kernel; - if (CUDA_SUCCESS != (err = p_cuLibraryGetKernel(&kernel, *h_library, name))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuLibraryGetKernel, &kernel, *h_library, name))) { return {}; } @@ -126,15 +126,16 @@ struct NvrtcProgramBox { } // namespace NvrtcProgramHandle create_nvrtc_program_handle(nvrtcProgram prog) { + // Resolve the table now, while the library that created `prog` is loaded, + // so the deleter never has to. + ensure_fn_table(FnTable::nvrtc); auto box = std::shared_ptr( new NvrtcProgramBox{prog}, [](NvrtcProgramBox* b) { // Note: nvrtcDestroyProgram takes nvrtcProgram* and nulls it, // but we're deleting the box anyway so nulling is harmless. - if (p_nvrtcDestroyProgram) { - GILReleaseGuard gil; - pw_nvrtcDestroyProgram(&b->resource); - } + GILReleaseGuard gil; + pw_nvrtcDestroyProgram(&b->resource); delete b; } ); @@ -157,16 +158,14 @@ struct NvvmProgramBox { } // namespace NvvmProgramHandle create_nvvm_program_handle(nvvmProgram prog) { + ensure_fn_table(FnTable::nvvm); auto box = std::shared_ptr( new NvvmProgramBox{{prog}}, [](NvvmProgramBox* b) { // Note: nvvmDestroyProgram takes nvvmProgram* and nulls it, // but we're deleting the box anyway so nulling is harmless. - // If NVVM is not available, the function pointer is null. - if (p_nvvmDestroyProgram) { - GILReleaseGuard gil; - pw_nvvmDestroyProgram(&b->resource.raw); - } + GILReleaseGuard gil; + pw_nvvmDestroyProgram(&b->resource.raw); delete b; } ); @@ -189,16 +188,14 @@ struct NvJitLinkBox { } // namespace NvJitLinkHandle create_nvjitlink_handle(nvJitLink_t handle) { + ensure_fn_table(FnTable::nvjitlink); auto box = std::shared_ptr( new NvJitLinkBox{{handle}}, [](NvJitLinkBox* b) { // Note: nvJitLinkDestroy takes nvJitLinkHandle* and nulls it, // but we're deleting the box anyway so nulling is harmless. - // If nvJitLink is not available, the function pointer is null. - if (p_nvJitLinkDestroy) { - GILReleaseGuard gil; - pw_nvJitLinkDestroy(&b->resource.raw); - } + GILReleaseGuard gil; + pw_nvJitLinkDestroy(&b->resource.raw); delete b; } ); @@ -221,14 +218,13 @@ struct CuLinkBox { } // namespace CuLinkHandle create_culink_handle(CUlinkState state) { + ensure_fn_table(FnTable::driver); auto box = std::shared_ptr( new CuLinkBox{state}, [](CuLinkBox* b) { // cuLinkDestroy takes CUlinkState by value (not pointer). - if (p_cuLinkDestroy) { - GILReleaseGuard gil; - pw_cuLinkDestroy(b->resource); - } + GILReleaseGuard gil; + pw_cuLinkDestroy(b->resource); delete b; } ); diff --git a/cuda_core/cuda/core/_cpp/rt/py_driver_fns.cpp b/cuda_core/cuda/core/_cpp/rt/py_driver_fns.cpp new file mode 100644 index 00000000000..bf4a31e8e1e --- /dev/null +++ b/cuda_core/cuda/core/_cpp/rt/py_driver_fns.cpp @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +// Filling the driver and compiler-library function tables from cuda-bindings. +// +// cuda-bindings loads each library and resolves its symbols once (for the +// driver, with cuGetProcAddress). cuda.bindings._internal. +// ._inspect_function_pointers() returns that table as {name: address}, where a +// zero address means the library does not provide the symbol. This file copies +// the entries cuda.core uses into the p_ pointers declared in driver_api.hpp. +// +// The fill runs Python, so it acquires the GIL and must never run under a C++ +// lock (the GIL is the outermost lock; see DESIGN.md). The slot stores happen +// under fill_mutex with no Python call inside, and the ready flag is published +// with release semantics after them, so readers that see the flag see the +// pointers. Two threads may both compute the table; they store identical +// values, one after the other. +// +// Failures never propagate as exceptions and never leave a Python error set: +// they are recorded, reported through report_message(), and every affected +// DRIVER_CALL then returns an error status from a trampoline (driver_api.hpp). + +#include "py.hpp" +#include "driver_api.hpp" +#include "error.hpp" +#include +#include +#include +#include +#include + +namespace cuda_core::rt { + +namespace { + +constexpr std::size_t kTables = 4; +constexpr std::size_t kMaxEntries = 128; + +std::atomic table_ready[kTables]; +std::atomic unavailable_reported[kTables]; +std::mutex fill_mutex; +char fill_error[kTables][512] = {}; + +std::size_t index_of(FnTable table) noexcept { return static_cast(table); } + +const char* module_name(FnTable table) noexcept { + switch (table) { + case FnTable::driver: return "cuda.bindings._internal.driver"; + case FnTable::nvrtc: return "cuda.bindings._internal.nvrtc"; + case FnTable::nvvm: return "cuda.bindings._internal.nvvm"; + case FnTable::nvjitlink: return "cuda.bindings._internal.nvjitlink"; + } + return "cuda.bindings._internal"; +} + +const char* library_name(FnTable table) noexcept { + switch (table) { + case FnTable::driver: return "CUDA driver"; + case FnTable::nvrtc: return "NVRTC"; + case FnTable::nvvm: return "NVVM"; + case FnTable::nvjitlink: return "nvJitLink"; + } + return "library"; +} + +// Copy the pending Python exception's text into buf and clear it. +void take_python_error(char* buf, std::size_t size) noexcept { +#if PY_VERSION_HEX >= 0x030C0000 + PyObject* exc = PyErr_GetRaisedException(); +#else + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + PyObject* exc = value; + Py_XDECREF(type); + Py_XDECREF(traceback); +#endif + PyObject* text = exc ? PyObject_Str(exc) : nullptr; + const char* utf8 = text ? PyUnicode_AsUTF8(text) : nullptr; + std::snprintf(buf, size, "%s", utf8 ? utf8 : "unknown error"); + Py_XDECREF(text); + Py_XDECREF(exc); + PyErr_Clear(); +} + +void record_failure(FnTable table, const char* message) noexcept { + { + std::lock_guard lock(fill_mutex); + std::snprintf(fill_error[index_of(table)], sizeof(fill_error[0]), "%s", message); + } + report_message(message); +} + +} // namespace + +bool fn_table_ready(FnTable table) noexcept { + return table_ready[index_of(table)].load(std::memory_order_acquire); +} + +const char* fn_table_error(FnTable table) noexcept { + const char* text = fill_error[index_of(table)]; + return text[0] ? text : nullptr; +} + +bool ensure_fn_table(FnTable table) noexcept { + const std::size_t idx = index_of(table); + if (table_ready[idx].load(std::memory_order_acquire)) { + return true; + } + std::size_t count = 0; + const FnEntry* entries = fn_table_entries(table, &count); + if (entries == nullptr || count > kMaxEntries) { + record_failure(table, "internal cuda.core error, please report: function table has an unexpected size"); + return false; + } + if (!Py_IsInitialized() || py_is_finalizing()) { + record_failure(table, "cuda.core cannot resolve driver functions while the interpreter is shutting down"); + return false; + } + + char message[640]; + char cause[384]; + void* values[kMaxEntries] = {}; + + GILAcquireGuard gil; + if (!gil.acquired()) { + record_failure(table, "cuda.core cannot resolve driver functions while the interpreter is shutting down"); + return false; + } + + PyObject* module = PyImport_ImportModule(module_name(table)); + if (module == nullptr) { + take_python_error(cause, sizeof(cause)); + std::snprintf(message, sizeof(message), + "cuda.core cannot import %s from the installed cuda-bindings: %s", module_name(table), cause); + record_failure(table, message); + return false; + } + PyObject* pointers = PyObject_CallMethod(module, "_inspect_function_pointers", nullptr); + Py_DECREF(module); + if (pointers == nullptr) { + take_python_error(cause, sizeof(cause)); + std::snprintf(message, sizeof(message), "cuda-bindings could not load the %s: %s", library_name(table), cause); + record_failure(table, message); + return false; + } + if (!PyDict_Check(pointers)) { + Py_DECREF(pointers); + std::snprintf(message, sizeof(message), + "internal cuda.core error, please report: %s._inspect_function_pointers() did not return a dict", + module_name(table)); + record_failure(table, message); + return false; + } + for (std::size_t i = 0; i < count; ++i) { + PyObject* item = PyDict_GetItemString(pointers, entries[i].key); // borrowed; no error on a miss + if (item == nullptr) { + Py_DECREF(pointers); + std::snprintf(message, sizeof(message), + "the installed cuda-bindings has no entry for %s (%s). cuda.core was compiled against a " + "cuda.h that names this symbol differently than the cuda-bindings in use; install the " + "cuda-bindings this cuda.core requires", + entries[i].name, entries[i].key); + record_failure(table, message); + return false; + } + void* address = PyLong_AsVoidPtr(item); + if (address == nullptr && PyErr_Occurred()) { + Py_DECREF(pointers); + take_python_error(cause, sizeof(cause)); + std::snprintf(message, sizeof(message), + "internal cuda.core error, please report: the entry for %s in %s is not an address: %s", + entries[i].key, module_name(table), cause); + record_failure(table, message); + return false; + } + values[i] = address; + } + Py_DECREF(pointers); + + // Every function introduced at or before the first release of the CUDA + // major series is present in every driver cuda.core supports; a null one + // means the driver is older than that. Newer functions may be null and are + // gated on the driver version in Cython. + if (table == FnTable::driver) { + for (std::size_t i = 0; i < count; ++i) { + if (values[i] == nullptr && entries[i].introduced <= CUDA_CORE_BUILD_MAJOR * 1000) { + std::snprintf(message, sizeof(message), + "the installed CUDA driver does not provide %s, which every driver of the CUDA %d " + "series provides; this cuda.core build requires a CUDA %d driver", + entries[i].name, CUDA_CORE_BUILD_MAJOR, CUDA_CORE_BUILD_MAJOR); + record_failure(table, message); + return false; + } + } + } + + { + std::lock_guard lock(fill_mutex); + if (!table_ready[idx].load(std::memory_order_relaxed)) { + for (std::size_t i = 0; i < count; ++i) { + *entries[i].slot = values[i]; + } + fill_error[idx][0] = 0; + table_ready[idx].store(true, std::memory_order_release); + } + } + return true; +} + +void report_unavailable_fn(FnTable table, const char* name) noexcept { + // Once per table: the first unavailable call is the informative one. + if (unavailable_reported[index_of(table)].exchange(true)) { + return; + } + char message[768]; + if (const char* reason = fn_table_error(table)) { + std::snprintf(message, sizeof(message), "cuda.core could not call %s: %s", name, reason); + } else { + std::snprintf(message, sizeof(message), + "internal cuda.core error, please report: %s was called but the installed %s does not " + "provide it; a feature gate is missing or wrong. The call returned an error instead.", + name, library_name(table)); + } + report_message(message); +} + +} // namespace cuda_core::rt diff --git a/cuda_core/cuda/core/_cpp/rt/stream.cpp b/cuda_core/cuda/core/_cpp/rt/stream.cpp index eb17b428f11..1cff809e50a 100644 --- a/cuda_core/cuda/core/_cpp/rt/stream.cpp +++ b/cuda_core/cuda/core/_cpp/rt/stream.cpp @@ -70,13 +70,12 @@ StreamHandle create_stream_handle(const ContextHandle& h_ctx, unsigned int flags CUstream stream = nullptr; GreenCtxHandle h_green = get_context_green_ctx(h_ctx); if (h_green) { - err = p_cuGreenCtxStreamCreate - ? p_cuGreenCtxStreamCreate(&stream, as_cu(h_green), flags, priority) - : CUDA_ERROR_NOT_SUPPORTED; + // Gated in Cython on driver >= 12.5 (cuGreenCtxStreamCreate's introduction). + err = DRIVER_CALL(cuGreenCtxStreamCreate, &stream, as_cu(h_green), flags, priority); } else { err = invoke_in_context_or_undo( h_ctx, - [&]() noexcept { return p_cuStreamCreateWithPriority(&stream, flags, priority); }, + [&]() noexcept { return DRIVER_CALL(cuStreamCreateWithPriority, &stream, flags, priority); }, [&]() noexcept { pw_cuStreamDestroy(stream); }, /*undo_requires_target_context=*/false); } diff --git a/cuda_core/cuda/core/_cpp/rt/texture.cpp b/cuda_core/cuda/core/_cpp/rt/texture.cpp index c667df28bd8..b3f34e08cd5 100644 --- a/cuda_core/cuda/core/_cpp/rt/texture.cpp +++ b/cuda_core/cuda/core/_cpp/rt/texture.cpp @@ -27,6 +27,7 @@ struct GraphicsResourceBox { } // namespace GraphicsResourceHandle create_graphics_resource_handle(CUgraphicsResource resource) { + ensure_fn_table(FnTable::driver); // the deleter calls the driver; resolve before it can run auto box = std::shared_ptr( new GraphicsResourceBox{resource}, [](const GraphicsResourceBox* b) { @@ -112,7 +113,7 @@ OpaqueArrayHandle create_array_handle(const ContextHandle& h_context, const CUDA CUarray arr = nullptr; err = invoke_in_context_or_undo( h_context, - [&]() noexcept { return p_cuArray3DCreate(&arr, &desc); }, + [&]() noexcept { return DRIVER_CALL(cuArray3DCreate, &arr, &desc); }, [&]() noexcept { pw_cuArrayDestroy(arr); }, /*undo_requires_target_context=*/false); if (err != CUDA_SUCCESS) { @@ -130,6 +131,7 @@ OpaqueArrayHandle create_array_handle_ref(CUarray arr) { } OpaqueArrayHandle create_array_handle_owning(CUarray arr) { + ensure_fn_table(FnTable::driver); // the deleter calls the driver; resolve before it can run if (!arr) { return {}; } @@ -145,7 +147,7 @@ OpaqueArrayHandle create_array_level_handle(const MipmappedArrayHandle& h_mip, u GILReleaseGuard gil; CUarray arr; ContextHandle h_context = h_mip ? get_box(h_mip)->h_context : ContextHandle{}; - if (CUDA_SUCCESS != (err = p_cuMipmappedArrayGetLevel(&arr, as_cu(h_mip), level))) { + if (CUDA_SUCCESS != (err = DRIVER_CALL(cuMipmappedArrayGetLevel, &arr, as_cu(h_mip), level))) { return {}; } // Non-owning level view: storage belongs to the mipmap. Embed the mipmap @@ -164,7 +166,7 @@ MipmappedArrayHandle create_mipmapped_array_handle(const ContextHandle& h_contex CUmipmappedArray mip = nullptr; err = invoke_in_context_or_undo( h_context, - [&]() noexcept { return p_cuMipmappedArrayCreate(&mip, &desc, num_levels); }, + [&]() noexcept { return DRIVER_CALL(cuMipmappedArrayCreate, &mip, &desc, num_levels); }, [&]() noexcept { pw_cuMipmappedArrayDestroy(mip); }, /*undo_requires_target_context=*/false); if (err != CUDA_SUCCESS) { @@ -195,7 +197,7 @@ TexObjectHandle make_tex_object_handle(const CUDA_RESOURCE_DESC& res, CUtexObject obj = 0; err = invoke_in_context_or_undo( h_context, - [&]() noexcept { return p_cuTexObjectCreate(&obj, &res, &tex, nullptr); }, + [&]() noexcept { return DRIVER_CALL(cuTexObjectCreate, &obj, &res, &tex, nullptr); }, [&]() noexcept { pw_cuTexObjectDestroy(obj); }, /*undo_requires_target_context=*/true); if (err != CUDA_SUCCESS) { @@ -206,7 +208,7 @@ TexObjectHandle make_tex_object_handle(const CUDA_RESOURCE_DESC& res, [](const TexObjectBox* b) { GILReleaseGuard gil; cleanup_in_context(b->h_context, "cuTexObjectDestroy", handle_bits(b->resource.raw), [&]() noexcept { - return p_cuTexObjectDestroy(b->resource.raw); + return DRIVER_CALL(cuTexObjectDestroy, b->resource.raw); }); delete b; } @@ -243,7 +245,7 @@ SurfObjectHandle create_surf_object_handle(const ContextHandle& h_context, CUsurfObject obj = 0; err = invoke_in_context_or_undo( h_context, - [&]() noexcept { return p_cuSurfObjectCreate(&obj, &res); }, + [&]() noexcept { return DRIVER_CALL(cuSurfObjectCreate, &obj, &res); }, [&]() noexcept { pw_cuSurfObjectDestroy(obj); }, /*undo_requires_target_context=*/true); if (err != CUDA_SUCCESS) { @@ -254,7 +256,7 @@ SurfObjectHandle create_surf_object_handle(const ContextHandle& h_context, [](const SurfObjectBox* b) { GILReleaseGuard gil; cleanup_in_context(b->h_context, "cuSurfObjectDestroy", handle_bits(b->resource.raw), [&]() noexcept { - return p_cuSurfObjectDestroy(b->resource.raw); + return DRIVER_CALL(cuSurfObjectDestroy, b->resource.raw); }); delete b; } diff --git a/cuda_core/cuda/core/_cpp/rt/types.hpp b/cuda_core/cuda/core/_cpp/rt/types.hpp index 59389f78fca..9b1ebaa6468 100644 --- a/cuda_core/cuda/core/_cpp/rt/types.hpp +++ b/cuda_core/cuda/core/_cpp/rt/types.hpp @@ -4,6 +4,7 @@ #pragma once +#include "versions.hpp" #include #include #include diff --git a/cuda_core/cuda/core/_cpp/rt/versions.hpp b/cuda_core/cuda/core/_cpp/rt/versions.hpp new file mode 100644 index 00000000000..eda836cb507 --- /dev/null +++ b/cuda_core/cuda/core/_cpp/rt/versions.hpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +// The one place the C++ under _cpp/ consults CUDA_VERSION. +// +// cuda.core supports one build configuration per CUDA major series: the +// cuda.h it compiles against has the same major.minor as the cuda-bindings it +// is built with, and that cuda-bindings is at or above the series' floor +// (cuda/core/_bindings_floor.py; https://github.com/NVIDIA/cuda-python/issues/2783). +// build_hooks.py enforces both before compiling and passes the decision down +// as two macros: +// +// CUDA_CORE_BUILD_MAJOR the CUDA major series being built (12 or 13); +// the only version the C++ may branch on, as +// `#if CUDA_CORE_BUILD_MAJOR >= 13`, and always +// for a difference between major series. +// CUDA_CORE_MIN_CUDA_VERSION the floor's major.minor as a CUDA_VERSION +// value (e.g. 13040). +// +// This header re-checks the header against both macros so that a build that +// bypasses build_hooks.py still cannot compile against an unsupported header. +// Minor-version fences (`#if CUDA_VERSION >= 130x0`) are not allowed anywhere +// else: they compiled features out of source builds against an older header +// while the run-time checks, which looked at the bindings and the driver, +// never noticed. tests/test_rt_layout.py enforces that this is the only file +// that names CUDA_VERSION. +// +// Downstream Cython code that cimports _rt includes this header without the +// macros; it then only learns the major from cuda.h and skips the floor check. + +#include + +#ifndef CUDA_CORE_BUILD_MAJOR +#define CUDA_CORE_BUILD_MAJOR (CUDA_VERSION / 1000) +#endif + +#if (CUDA_VERSION / 1000) != CUDA_CORE_BUILD_MAJOR +#error "cuda.h does not belong to the CUDA major series cuda.core is being built for (CUDA_CORE_BUILD_MAJOR)" +#endif + +#ifdef CUDA_CORE_MIN_CUDA_VERSION +#if CUDA_VERSION < CUDA_CORE_MIN_CUDA_VERSION +#error "cuda.h is older than the minimum this cuda.core release supports for its CUDA major series (see the cuda.core support policy)" +#endif +#endif diff --git a/cuda_core/cuda/core/_device.pyx b/cuda_core/cuda/core/_device.pyx index 40b09d6c761..f7901081c98 100644 --- a/cuda_core/cuda/core/_device.pyx +++ b/cuda_core/cuda/core/_device.pyx @@ -1057,13 +1057,6 @@ class Device: cuda.core.system.Device The corresponding system-level device instance used for NVML access. """ - from cuda.core.system._system import CUDA_BINDINGS_NVML_IS_COMPATIBLE - - if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: - raise RuntimeError( - "cuda.core.system.Device requires cuda-bindings 12.9.6+ for CUDA 12.x, or cuda-bindings 13.2.0+ for CUDA 13.x" - ) - from cuda.core.system import Device as SystemDevice return SystemDevice(uuid=self.uuid) @@ -1647,11 +1640,9 @@ cdef inline int Device_ensure_cuda_initialized() except? -1: with _lock, nogil: HANDLE_RETURN(cydriver.cuInit(0)) _is_cuInit = True - try: + IF CUDA_CORE_BUILD_MAJOR >= 13: + # Added in cuda-bindings 13.3; absent from the 12.x line. from cuda.bindings.utils import warn_if_cuda_major_version_mismatch - except ImportError: - pass - else: warn_if_cuda_major_version_mismatch() return 0 diff --git a/cuda_core/cuda/core/_device_resources.pyx b/cuda_core/cuda/core/_device_resources.pyx index c9fe33c4fd7..0f6e94bf6b3 100644 --- a/cuda_core/cuda/core/_device_resources.pyx +++ b/cuda_core/cuda/core/_device_resources.pyx @@ -20,7 +20,7 @@ from cuda.bindings cimport cydriver from cuda.core._rt cimport ContextHandle, GreenCtxHandle, as_cu, get_context_green_ctx from cuda.core._utils.cuda_utils cimport check_or_create_options, HANDLE_RETURN from cuda.core._utils.cuda_utils import is_sequence -from cuda.core._utils.version cimport cy_binding_version, cy_driver_version +from cuda.core._utils.version cimport cy_driver_version from cuda.core._utils.validators import check_str_enum @@ -47,7 +47,6 @@ cdef inline int _check_green_ctx_support() except?-1: if _green_ctx_checked == -1: raise RuntimeError(_green_ctx_err_msg) cdef tuple drv = cy_driver_version() - cdef tuple bind = cy_binding_version() if drv < (12, 4, 0): _green_ctx_err_msg = ( "Green context support requires CUDA driver 12.4 or newer " @@ -55,13 +54,6 @@ cdef inline int _check_green_ctx_support() except?-1: ) _green_ctx_checked = -1 raise RuntimeError(_green_ctx_err_msg) - if bind < (12, 4, 0): - _green_ctx_err_msg = ( - "Green context support requires cuda.bindings 12.4 or newer " - f"(current bindings: {'.'.join(map(str, bind))})" - ) - _green_ctx_checked = -1 - raise RuntimeError(_green_ctx_err_msg) _green_ctx_checked = 1 return 0 @@ -73,7 +65,6 @@ cdef inline int _check_workqueue_support() except?-1: if _workqueue_checked == -1: raise RuntimeError(_workqueue_err_msg) cdef tuple drv = cy_driver_version() - cdef tuple bind = cy_binding_version() if drv < (13, 1, 0): _workqueue_err_msg = ( "WorkqueueResource requires CUDA driver 13.1 or newer " @@ -81,13 +72,6 @@ cdef inline int _check_workqueue_support() except?-1: ) _workqueue_checked = -1 raise RuntimeError(_workqueue_err_msg) - if bind < (13, 1, 0): - _workqueue_err_msg = ( - "WorkqueueResource requires cuda.bindings 13.1 or newer " - f"(current bindings: {'.'.join(map(str, bind))})" - ) - _workqueue_checked = -1 - raise RuntimeError(_workqueue_err_msg) _workqueue_checked = 1 return 0 @@ -225,20 +209,17 @@ cdef inline unsigned int _to_sm_count(object value) except? 0: return (value) -IF CUDA_CORE_BUILD_MAJOR >= 13: - from cuda.core._rt cimport sm_resource_split, has_sm_resource_split - cdef int _structured_split_checked = 0 cdef inline bint _can_use_structured_sm_split(): - """Check if cuDevSmResourceSplit (13.1+) is available. Cached.""" + """Whether the driver provides cuDevSmResourceSplit (13.1+). Cached. + + cuda-bindings 13.4+ (the floor) always exports it; only the driver can lack it.""" global _structured_split_checked if _structured_split_checked != 0: return _structured_split_checked == 1 IF CUDA_CORE_BUILD_MAJOR >= 13: - if (has_sm_resource_split() - and cy_driver_version() >= (13, 1, 0) - and cy_binding_version() >= (13, 1, 0)): + if cy_driver_version() >= (13, 1, 0): _structured_split_checked = 1 return True _structured_split_checked = -1 @@ -326,13 +307,13 @@ IF CUDA_CORE_BUILD_MAJOR >= 13: memset(&remaining, 0, sizeof(cydriver.CUdevResource)) with nogil: - HANDLE_RETURN(sm_resource_split( + HANDLE_RETURN(cydriver.cuDevSmResourceSplit( result, (n_groups), &sm._resource, &remaining, 0, - params, + params, )) if result != NULL: diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index 3e68d24afa3..cdd4373d478 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -30,7 +30,6 @@ from typing import TYPE_CHECKING, Union from warnings import warn from cuda.pathfinder import DynamicLibNotFoundError -from cuda.pathfinder._optional_cuda_import import _optional_cuda_import from cuda.core._device import Device from cuda.core._module import ObjectCode from cuda.core._utils.clear_error_support import assert_type @@ -728,26 +727,24 @@ def _decide_nvjitlink_or_driver() -> bool: " For best results, consider upgrading to a recent version of" ) - nvjitlink_module = _optional_cuda_import("cuda.bindings.nvjitlink") - if nvjitlink_module is None: - warn_txt = f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." - else: - from cuda.bindings._internal import nvjitlink + # cuda.bindings.nvjitlink is present in every cuda-bindings cuda.core accepts; + # only the nvJitLink library itself can be missing or too old. + from cuda.bindings._internal import nvjitlink - try: - has_version_symbol = _nvjitlink_has_version_symbol(nvjitlink) - except DynamicLibNotFoundError: - warn_txt = ( - f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." - ) - else: - if has_version_symbol: - _use_nvjitlink_backend = True - return False # Use nvjitlink - warn_txt = ( - f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)." - f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink." - ) + try: + has_version_symbol = _nvjitlink_has_version_symbol(nvjitlink) + except DynamicLibNotFoundError: + warn_txt = ( + f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." + ) + else: + if has_version_symbol: + _use_nvjitlink_backend = True + return False # Use nvjitlink + warn_txt = ( + f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)." + f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink." + ) warn(warn_txt, stacklevel=2, category=RuntimeWarning) _driver = driver diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index ae4546eb5be..e6c4c6710bb 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -30,9 +30,6 @@ from cuda.core.typing import DevicePointerType from cuda.core._memory._copy_attributes cimport _with_attributes_available from cuda.core._memory._copy_attributes cimport _to_cu_memcpy_attributes # no-cython-lint -IF CUDA_CORE_BUILD_MAJOR >= 13: - from cuda.core._rt cimport memcpy_with_attributes_async - from cuda.core._stream cimport Stream, Stream_accept, Stream_is_legacy_default_token, default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value @@ -187,11 +184,9 @@ cdef void _do_copy_with_attributes( object options, cydriver.CUstream hstream, ): IF CUDA_CORE_BUILD_MAJOR >= 13: - # Routed through the memcpy_with_attributes_async() C++ shim since - # cydriver.cuMemcpyWithAttributesAsync is absent from cuda-bindings < 13.2. cdef cydriver.CUmemcpyAttributes cu_attr = _to_cu_memcpy_attributes(options) with nogil: - HANDLE_RETURN(memcpy_with_attributes_async(dst, src, nbytes, &cu_attr, hstream)) + HANDLE_RETURN(cydriver.cuMemcpyWithAttributesAsync(dst, src, nbytes, &cu_attr, hstream)) ELSE: pass # unreachable: _with_attributes_available() is always False on CUDA 12 diff --git a/cuda_core/cuda/core/_memory/_copy_attributes.pxd b/cuda_core/cuda/core/_memory/_copy_attributes.pxd index 3e4ae2e778f..e85d1b71a99 100644 --- a/cuda_core/cuda/core/_memory/_copy_attributes.pxd +++ b/cuda_core/cuda/core/_memory/_copy_attributes.pxd @@ -7,23 +7,14 @@ # without either depending on the other. from cuda.bindings cimport cydriver -from cuda.core._utils.version cimport cy_binding_version, cy_driver_version # no-cython-lint +from cuda.core._utils.version cimport cy_driver_version # no-cython-lint IF CUDA_CORE_BUILD_MAJOR >= 13: - from cuda.core._rt cimport has_memcpy_with_attributes_async - cdef inline bint _with_attributes_available(): - # has_memcpy_with_attributes_async() says whether the installed - # cuda-bindings actually exports cuMemcpyWithAttributesAsync (13.2+); - # the version checks alone are not sufficient, since cuda.core's build - # can be paired with a cuda-bindings install older than what it built - # against (see https://github.com/NVIDIA/cuda-python/issues/2063). - return ( - has_memcpy_with_attributes_async() - and cy_driver_version() >= (13, 2, 0) - and cy_binding_version() >= (13, 2, 0) - ) + # cuMemcpyWithAttributesAsync is a 13.2 driver API; cuda-bindings 13.4+ + # (the floor) always exports it, so only the driver can lack it. + return cy_driver_version() >= (13, 2, 0) ELSE: cdef inline bint _with_attributes_available(): return False diff --git a/cuda_core/cuda/core/_memory/_copy_enums.py b/cuda_core/cuda/core/_memory/_copy_enums.py index 84c72e71110..8b53708ee26 100644 --- a/cuda_core/cuda/core/_memory/_copy_enums.py +++ b/cuda_core/cuda/core/_memory/_copy_enums.py @@ -11,7 +11,6 @@ from cuda.core._host import Host from cuda.core._utils.cuda_utils import driver from cuda.core._utils.pycompat import StrEnum -from cuda.core._utils.version import binding_version __all__ = ["CopyOptions", "MemcpyOverlapMode", "MemcpySrcAccessOrder"] @@ -117,47 +116,31 @@ def __post_init__(self): def _to_driver_enum(self) -> int: """Return the driver CUmemcpySrcAccessOrder value.""" - if not _SRC_ACCESS_ORDER_TO_DRIVER: - raise NotImplementedError(_CUDA13_REQUIRED) return _SRC_ACCESS_ORDER_TO_DRIVER[MemcpySrcAccessOrder(self.src_access_order)] def _to_driver_flags(self) -> int: """Return the driver CUmemcpyFlags value.""" - if not _OVERLAP_MODE_TO_DRIVER: - raise NotImplementedError(_CUDA13_REQUIRED) return _OVERLAP_MODE_TO_DRIVER[MemcpyOverlapMode(self.overlap_mode)] -_CUDA13_REQUIRED = "copy attributes require cuda.bindings 13.0 or newer" - -# CUmemcpySrcAccessOrder and CUmemcpyFlags are exposed by cuda.bindings 13.0+, -# so these maps are empty when it is older. Nothing reaches them there: -# copy_batch refuses non-default CopyOptions when the batched entry point is -# unavailable. -# -# Keyed by ``str``: under ``python_version = "3.10"`` mypy resolves StrEnum to -# the unstubbed backports shim and so infers the members as plain ``str``. -# StrEnum members are ``str`` instances, so this holds on every version. The -# values are wrapped in ``int()`` because the driver enums are untyped. -_SRC_ACCESS_ORDER_TO_DRIVER: dict[str, int] -_OVERLAP_MODE_TO_DRIVER: dict[str, int] - -if binding_version() >= (13, 0, 0): - _src_order = driver.CUmemcpySrcAccessOrder - _flags = driver.CUmemcpyFlags - _SRC_ACCESS_ORDER_TO_DRIVER = { - MemcpySrcAccessOrder.STREAM: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_STREAM), - MemcpySrcAccessOrder.DURING_API_CALL: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_DURING_API_CALL), - MemcpySrcAccessOrder.ANY: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_ANY), - } - _OVERLAP_MODE_TO_DRIVER = { - MemcpyOverlapMode.DEFAULT: int(_flags.CU_MEMCPY_FLAG_DEFAULT), - MemcpyOverlapMode.PREFER_OVERLAP_WITH_COMPUTE: int(_flags.CU_MEMCPY_FLAG_PREFER_OVERLAP_WITH_COMPUTE), - } - del _src_order, _flags -else: - _SRC_ACCESS_ORDER_TO_DRIVER = {} - _OVERLAP_MODE_TO_DRIVER = {} +# CUmemcpySrcAccessOrder and CUmemcpyFlags were added in CUDA 12.8; every +# cuda-bindings cuda.core accepts has them. Keyed by ``str``: under +# ``python_version = "3.10"`` mypy resolves StrEnum to the unstubbed backports +# shim and so infers the members as plain ``str``. StrEnum members are ``str`` +# instances, so this holds on every version. The values are wrapped in +# ``int()`` because the driver enums are untyped. +_src_order = driver.CUmemcpySrcAccessOrder +_flags = driver.CUmemcpyFlags +_SRC_ACCESS_ORDER_TO_DRIVER: dict[str, int] = { + MemcpySrcAccessOrder.STREAM: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_STREAM), + MemcpySrcAccessOrder.DURING_API_CALL: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_DURING_API_CALL), + MemcpySrcAccessOrder.ANY: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_ANY), +} +_OVERLAP_MODE_TO_DRIVER: dict[str, int] = { + MemcpyOverlapMode.DEFAULT: int(_flags.CU_MEMCPY_FLAG_DEFAULT), + MemcpyOverlapMode.PREFER_OVERLAP_WITH_COMPUTE: int(_flags.CU_MEMCPY_FLAG_PREFER_OVERLAP_WITH_COMPUTE), +} +del _src_order, _flags def _reject_unsupported_during_api_call( diff --git a/cuda_core/cuda/core/_memory/_managed_buffer.py b/cuda_core/cuda/core/_memory/_managed_buffer.py index 44b84ff241e..4353fb7975d 100644 --- a/cuda_core/cuda/core/_memory/_managed_buffer.py +++ b/cuda_core/cuda/core/_memory/_managed_buffer.py @@ -19,7 +19,7 @@ _read_preferred_location_v2, ) from cuda.core._utils.cuda_utils import driver, handle_return -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version if TYPE_CHECKING: from cuda.core._memory._buffer import MemoryResource @@ -215,13 +215,13 @@ def preferred_location(self) -> Device | Host | None: as ``Host()``. """ # The v2 path uses CU_MEM_RANGE_ATTRIBUTE_PREFERRED_LOCATION_{TYPE,ID}, - # both added in CUDA 13. Require both bindings and the runtime driver - # to be 13.0+; otherwise fall back to the legacy device-ordinal path. - # See PR #2054 / #2064 for prior bindings-only-check regressions. - if binding_version() >= (13, 0, 0) and driver_version() >= (13, 0, 0): + # both added in CUDA 13: it exists in the CUDA 13 build only, and the + # runtime driver must be 13.0+ too; otherwise fall back to the legacy + # device-ordinal path. See PR #2054 / #2064 for prior regressions. + if BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0): return _read_preferred_location_v2(self) - # CUDA 12 legacy path (no NUMA info available; also taken when - # bindings are 13.x but the runtime driver is still 12.x). + # CUDA 12 legacy path (no NUMA info available; also taken by a CUDA 13 + # build when the runtime driver is still 12.x). loc_id = _get_int_attr(self, _ATTR_PREFERRED) if loc_id == -2: return None @@ -249,7 +249,7 @@ def last_prefetch_location(self) -> Device | Host | None: the legacy attribute carries only a device ordinal (or ``-1`` for host), so host NUMA details are unavailable. """ - if binding_version() >= (13, 0, 0) and driver_version() >= (13, 0, 0): + if BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0): return _read_last_prefetch_location_v2(self) loc_id = _get_int_attr(self, _ATTR_LAST_PREFETCH) if loc_id == -2: diff --git a/cuda_core/cuda/core/_memory/_managed_location.py b/cuda_core/cuda/core/_memory/_managed_location.py index 336f09f2eee..e77f242de7f 100644 --- a/cuda_core/cuda/core/_memory/_managed_location.py +++ b/cuda_core/cuda/core/_memory/_managed_location.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Literal -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version if TYPE_CHECKING: from cuda.core._device import Device @@ -39,13 +39,14 @@ def _reject_numa_host_on_cuda12(spec: _LocSpec) -> None: ``TypeError`` at the call boundary with actionable wording. """ # The host-NUMA kinds map to CU_MEM_LOCATION_TYPE_HOST_NUMA{,_CURRENT}, - # both added in CUDA 13. Require both bindings and the runtime driver to - # be 13.0+; bindings-only is insufficient (PR #2054 / #2064 precedent). - if binding_version() >= (13, 0, 0) and driver_version() >= (13, 0, 0): + # both added in CUDA 13: the CUDA 13 build passes them to the v2 driver + # entry points, which need a 13.0+ runtime driver as well (a build check + # alone is insufficient; PR #2054 / #2064 precedent). + if BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0): return if spec.kind in ("host_numa", "host_numa_current"): raise TypeError( - "Host(numa_id=...) / Host.numa_current() require both cuda-bindings 13.0+ " + "Host(numa_id=...) / Host.numa_current() require the CUDA 13 build of cuda.core " "and a CUDA 13+ runtime driver; use Host() instead" ) diff --git a/cuda_core/cuda/core/_memory/_managed_memory_ops.pyx b/cuda_core/cuda/core/_memory/_managed_memory_ops.pyx index 61caa59ce1f..77cf423fab5 100644 --- a/cuda_core/cuda/core/_memory/_managed_memory_ops.pyx +++ b/cuda_core/cuda/core/_memory/_managed_memory_ops.pyx @@ -91,7 +91,7 @@ IF CUDA_CORE_BUILD_MAJOR < 13: if kind == "host": return -1 raise RuntimeError( - "Host(numa_id=...) / Host.numa_current() require both cuda-bindings 13.0+ " + "Host(numa_id=...) / Host.numa_current() require the CUDA 13 build of cuda.core " "and a CUDA 13+ runtime driver; use Host() instead" ) diff --git a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py index ea1e2455c6f..bdc57595f53 100644 --- a/cuda_core/cuda/core/_memory/_virtual_memory_resource.py +++ b/cuda_core/cuda/core/_memory/_virtual_memory_resource.py @@ -21,7 +21,7 @@ from cuda.core._utils.cuda_utils import ( _check_driver_error as raise_if_driver_error, ) -from cuda.core._utils.version import binding_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR from cuda.core.typing import ( DevicePointerType, VirtualMemoryAccessType, @@ -112,9 +112,9 @@ class VirtualMemoryResourceOptions: VirtualMemoryLocationType.HOST_NUMA_CURRENT: _l.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT, } _t = driver.CUmemAllocationType - # CUDA 13+ exposes MANAGED in CUmemAllocationType; older 12.x does not + # CUDA 13 added MANAGED to CUmemAllocationType; the CUDA 12 build has no such member. _allocation_type = {VirtualMemoryAllocationType.PINNED: _t.CU_MEM_ALLOCATION_TYPE_PINNED} # noqa: RUF012 - if binding_version() >= (13, 0, 0): + if BUILD_CUDA_MAJOR >= 13: _allocation_type[VirtualMemoryAllocationType.MANAGED] = _t.CU_MEM_ALLOCATION_TYPE_MANAGED @staticmethod diff --git a/cuda_core/cuda/core/_module.pyx b/cuda_core/cuda/core/_module.pyx index fbaaef9e1e8..0264ebad7e9 100644 --- a/cuda_core/cuda/core/_module.pyx +++ b/cuda_core/cuda/core/_module.pyx @@ -35,7 +35,7 @@ from cuda.core._utils.clear_error_support import ( raise_code_path_meant_to_be_unreachable, ) from cuda.core._utils.cuda_utils cimport HANDLE_RETURN -from cuda.core._utils.version cimport cy_binding_version, cy_driver_version +from cuda.core._utils.version cimport cy_driver_version from cuda.core._utils.cuda_utils import driver from cuda.bindings cimport cydriver @@ -472,11 +472,6 @@ cdef class Kernel: "Driver version 12.4 or newer is required for this function. " f"Using driver version {'.'.join(map(str, cy_driver_version()))}" ) - if cy_binding_version() < (12, 4, 0): - raise NotImplementedError( - "cuda.bindings 12.4 or newer is required for this function. " - f"Using binding version {'.'.join(map(str, cy_binding_version()))}" - ) cdef size_t arg_pos = 0 cdef list param_info_data = [] cdef cydriver.CUkernel cu_kernel = as_cu(self._h_kernel) diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index fd4a31ba1e8..f28f63ff067 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -46,7 +46,7 @@ from cuda.core._utils.cuda_utils import ( is_nested_sequence, is_sequence, ) -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import driver_version from cuda.core.utils._cache_dir import _default_cache_dir from cuda.core.typing import ObjectCodeFormatType, CompilerBackendType, PCHStatusType, SourceCodeType @@ -758,13 +758,8 @@ def _get_nvvm_module() -> object: raise RuntimeError("NVVM module is not available (previous import attempt failed)") try: - version = binding_version() - if version < (12, 9, 0): - raise RuntimeError( - f"NVVM bindings require cuda-bindings >= 12.9.0, but found {'.'.join(map(str, version))}. " - "Please update cuda-bindings to use NVVM features." - ) - + # cuda.bindings.nvvm is present in every cuda-bindings cuda.core accepts; + # the probe checks that libnvvm itself can be loaded. nvvm = _optional_cuda_import( "cuda.bindings.nvvm", probe_function=lambda module: module.version(), # probe triggers libnvvm load @@ -1046,15 +1041,6 @@ cdef object _nvrtc_compile_and_extract( return ObjectCode._init(bytes(data), target_type, symbol_mapping=symbol_mapping, name=name) -cdef int _nvrtc_pch_apis_cached = -1 # -1 = unchecked - -cdef bint _has_nvrtc_pch_apis(): - global _nvrtc_pch_apis_cached - if _nvrtc_pch_apis_cached < 0: - _nvrtc_pch_apis_cached = hasattr(nvrtc, "nvrtcGetPCHCreateStatus") - return _nvrtc_pch_apis_cached - - cdef object _read_pch_status(cynvrtc.nvrtcProgram prog): """Query nvrtcGetPCHCreateStatus and translate to a high-level string.""" cdef cynvrtc.nvrtcResult err @@ -1079,7 +1065,7 @@ cdef object Program_compile_nvrtc(Program self, str target_type, object name_exp ) cdef bint pch_creation_possible = self._options.create_pch or self._options.pch - if not pch_creation_possible or not _has_nvrtc_pch_apis(): + if not pch_creation_possible: self._pch_status = None return result diff --git a/cuda_core/cuda/core/_rt.pxd b/cuda_core/cuda/core/_rt.pxd index 76082d7ec0e..e12cefb0f71 100644 --- a/cuda_core/cuda/core/_rt.pxd +++ b/cuda_core/cuda/core/_rt.pxd @@ -375,20 +375,3 @@ cdef TexObjectHandle create_tex_object_handle_linear( cdef SurfObjectHandle create_surf_object_handle( const ContextHandle& h_context, const cydriver.CUDA_RESOURCE_DESC& res, const OpaqueArrayHandle& h_backing) except+ nogil - -# SM resource split (13.1+ — calls through function pointer, safe on older bindings) -# groupParams is void* here to avoid referencing CU_DEV_SM_RESOURCE_GROUP_PARAMS -# (which doesn't exist in cuda-bindings 13.0 .pxd). The C++ side casts it. -cdef cydriver.CUresult sm_resource_split( - cydriver.CUdevResource* result, unsigned int nbGroups, - const cydriver.CUdevResource* input, cydriver.CUdevResource* remainder, - unsigned int flags, void* groupParams) nogil -cdef bint has_sm_resource_split() noexcept nogil - -# cuMemcpyWithAttributesAsync (13.2+ — calls through function pointer, safe on older bindings) -# attr is void* here to avoid referencing CUmemcpyAttributes (absent from -# cuda-bindings built against CUDA < 12.8). The C++ side casts it. -cdef cydriver.CUresult memcpy_with_attributes_async( - cydriver.CUdeviceptr dst, cydriver.CUdeviceptr src, size_t size, - void* attr, cydriver.CUstream hStream) nogil -cdef bint has_memcpy_with_attributes_async() noexcept nogil diff --git a/cuda_core/cuda/core/_rt.pyx b/cuda_core/cuda/core/_rt.pyx index f92b051c0d7..74ec27f942f 100644 --- a/cuda_core/cuda/core/_rt.pyx +++ b/cuda_core/cuda/core/_rt.pyx @@ -15,7 +15,6 @@ # without needing separate wrapper functions. from cpython.object cimport PyObject -from cpython.pycapsule cimport PyCapsule_GetName, PyCapsule_GetPointer from libc.stddef cimport size_t from cuda.bindings cimport cydriver @@ -23,10 +22,6 @@ from cuda.bindings cimport cynvrtc from cuda.bindings cimport cynvvm from cuda.bindings cimport cynvjitlink -import cuda.bindings.cydriver as cydriver -import cuda.bindings.cynvrtc as cynvrtc -import cuda.bindings.cynvvm as cynvvm -import cuda.bindings.cynvjitlink as cynvjitlink # ============================================================================= # C++ function declarations (non-inline, implemented under _cpp/rt/) @@ -271,23 +266,6 @@ cdef extern from "_cpp/rt/rt.hpp" namespace "cuda_core::rt": FileDescriptorHandle create_fd_handle_ref "cuda_core::rt::create_fd_handle_ref" ( int fd) except+ nogil - # SM resource split (13.1+ wrapper — avoids direct cydriver cimport) - # groupParams is void* to avoid referencing CU_DEV_SM_RESOURCE_GROUP_PARAMS - # (which doesn't exist in cuda-bindings 13.0 .pxd). The C++ side casts it. - cydriver.CUresult sm_resource_split "cuda_core::rt::sm_resource_split" ( - cydriver.CUdevResource* result, unsigned int nbGroups, - const cydriver.CUdevResource* input, cydriver.CUdevResource* remainder, - unsigned int flags, void* groupParams) nogil - bint has_sm_resource_split "cuda_core::rt::has_sm_resource_split" () noexcept nogil - - # cuMemcpyWithAttributesAsync (13.2+ wrapper — avoids direct cydriver cimport) - # attr is void* to avoid referencing CUmemcpyAttributes (absent from - # cuda-bindings built against CUDA < 12.8). The C++ side casts it. - cydriver.CUresult memcpy_with_attributes_async "cuda_core::rt::memcpy_with_attributes_async" ( - cydriver.CUdeviceptr dst, cydriver.CUdeviceptr src, size_t size, - void* attr, cydriver.CUstream hStream) nogil - bint has_memcpy_with_attributes_async "cuda_core::rt::has_memcpy_with_attributes_async" () noexcept nogil - # Array / mipmapped-array / texture / surface handles (PR #467) OpaqueArrayHandle create_array_handle "cuda_core::rt::create_array_handle" ( const ContextHandle& h_context, const cydriver.CUDA_ARRAY3D_DESCRIPTOR& desc) except+ nogil @@ -318,265 +296,6 @@ cdef extern from "_cpp/rt/rt.hpp" namespace "cuda_core::rt": const OpaqueArrayHandle& h_backing) except+ nogil -# ============================================================================= -# CUDA driver function pointer initialization -# -# The C++ code declares extern function pointers (p_cuXxx) that need to be -# populated before any handle creation functions are called. We extract these -# from cuda.bindings.cydriver.__pyx_capi__ at module import time. -# -# The Cython string substitution (e.g., "reinterpret_cast(...)") -# allows us to assign void* values to typed function pointer variables. -# ============================================================================= - -# Declare extern variables with reinterpret_cast to allow void* assignment -cdef extern from "_cpp/rt/rt.hpp" namespace "cuda_core::rt": - # Error formatting - void* p_cuGetErrorName "reinterpret_cast(cuda_core::rt::p_cuGetErrorName)" - void* p_cuGetErrorString "reinterpret_cast(cuda_core::rt::p_cuGetErrorString)" - - # Context - void* p_cuDevicePrimaryCtxRetain "reinterpret_cast(cuda_core::rt::p_cuDevicePrimaryCtxRetain)" - void* p_cuDevicePrimaryCtxRelease "reinterpret_cast(cuda_core::rt::p_cuDevicePrimaryCtxRelease)" - void* p_cuCtxGetCurrent "reinterpret_cast(cuda_core::rt::p_cuCtxGetCurrent)" - void* p_cuCtxSetCurrent "reinterpret_cast(cuda_core::rt::p_cuCtxSetCurrent)" - void* p_cuCtxSynchronize "reinterpret_cast(cuda_core::rt::p_cuCtxSynchronize)" - void* p_cuCtxGetStreamPriorityRange "reinterpret_cast(cuda_core::rt::p_cuCtxGetStreamPriorityRange)" - void* p_cuCtxGetDevice "reinterpret_cast(cuda_core::rt::p_cuCtxGetDevice)" - void* p_cuGraphNodeSetParams "reinterpret_cast(cuda_core::rt::p_cuGraphNodeSetParams)" - void* p_cuGreenCtxCreate "reinterpret_cast(cuda_core::rt::p_cuGreenCtxCreate)" - void* p_cuGreenCtxDestroy "reinterpret_cast(cuda_core::rt::p_cuGreenCtxDestroy)" - void* p_cuCtxFromGreenCtx "reinterpret_cast(cuda_core::rt::p_cuCtxFromGreenCtx)" - void* p_cuDevResourceGenerateDesc "reinterpret_cast(cuda_core::rt::p_cuDevResourceGenerateDesc)" - void* p_cuGreenCtxStreamCreate "reinterpret_cast(cuda_core::rt::p_cuGreenCtxStreamCreate)" - - # Stream - void* p_cuStreamCreateWithPriority "reinterpret_cast(cuda_core::rt::p_cuStreamCreateWithPriority)" - void* p_cuStreamDestroy "reinterpret_cast(cuda_core::rt::p_cuStreamDestroy)" - void* p_cuStreamGetCtx "reinterpret_cast(cuda_core::rt::p_cuStreamGetCtx)" - - # Event - void* p_cuEventCreate "reinterpret_cast(cuda_core::rt::p_cuEventCreate)" - void* p_cuEventDestroy "reinterpret_cast(cuda_core::rt::p_cuEventDestroy)" - void* p_cuIpcOpenEventHandle "reinterpret_cast(cuda_core::rt::p_cuIpcOpenEventHandle)" - - # Device - void* p_cuDeviceGetCount "reinterpret_cast(cuda_core::rt::p_cuDeviceGetCount)" - - # Memory pool - void* p_cuMemPoolSetAccess "reinterpret_cast(cuda_core::rt::p_cuMemPoolSetAccess)" - void* p_cuMemPoolDestroy "reinterpret_cast(cuda_core::rt::p_cuMemPoolDestroy)" - void* p_cuMemPoolCreate "reinterpret_cast(cuda_core::rt::p_cuMemPoolCreate)" - void* p_cuDeviceGetMemPool "reinterpret_cast(cuda_core::rt::p_cuDeviceGetMemPool)" - void* p_cuMemPoolImportFromShareableHandle "reinterpret_cast(cuda_core::rt::p_cuMemPoolImportFromShareableHandle)" - - # Memory allocation - void* p_cuMemAllocFromPoolAsync "reinterpret_cast(cuda_core::rt::p_cuMemAllocFromPoolAsync)" - void* p_cuMemAllocAsync "reinterpret_cast(cuda_core::rt::p_cuMemAllocAsync)" - void* p_cuMemAlloc "reinterpret_cast(cuda_core::rt::p_cuMemAlloc)" - void* p_cuMemAllocHost "reinterpret_cast(cuda_core::rt::p_cuMemAllocHost)" - - # Memory deallocation - void* p_cuMemFreeAsync "reinterpret_cast(cuda_core::rt::p_cuMemFreeAsync)" - void* p_cuMemFree "reinterpret_cast(cuda_core::rt::p_cuMemFree)" - void* p_cuMemFreeHost "reinterpret_cast(cuda_core::rt::p_cuMemFreeHost)" - - # IPC - void* p_cuMemPoolImportPointer "reinterpret_cast(cuda_core::rt::p_cuMemPoolImportPointer)" - - # Library - void* p_cuLibraryLoadFromFile "reinterpret_cast(cuda_core::rt::p_cuLibraryLoadFromFile)" - void* p_cuLibraryLoadData "reinterpret_cast(cuda_core::rt::p_cuLibraryLoadData)" - void* p_cuLibraryUnload "reinterpret_cast(cuda_core::rt::p_cuLibraryUnload)" - void* p_cuLibraryGetKernel "reinterpret_cast(cuda_core::rt::p_cuLibraryGetKernel)" - - # Graph - void* p_cuGraphDestroy "reinterpret_cast(cuda_core::rt::p_cuGraphDestroy)" - void* p_cuGraphInstantiateWithParams "reinterpret_cast(cuda_core::rt::p_cuGraphInstantiateWithParams)" - void* p_cuGraphExecUpdate "reinterpret_cast(cuda_core::rt::p_cuGraphExecUpdate)" - void* p_cuGraphExecDestroy "reinterpret_cast(cuda_core::rt::p_cuGraphExecDestroy)" - void* p_cuUserObjectCreate "reinterpret_cast(cuda_core::rt::p_cuUserObjectCreate)" - void* p_cuUserObjectRelease "reinterpret_cast(cuda_core::rt::p_cuUserObjectRelease)" - void* p_cuGraphRetainUserObject "reinterpret_cast(cuda_core::rt::p_cuGraphRetainUserObject)" - void* p_cuGraphReleaseUserObject "reinterpret_cast(cuda_core::rt::p_cuGraphReleaseUserObject)" - void* p_cuGraphNodeFindInClone "reinterpret_cast(cuda_core::rt::p_cuGraphNodeFindInClone)" - void* p_cuGraphChildGraphNodeGetGraph "reinterpret_cast(cuda_core::rt::p_cuGraphChildGraphNodeGetGraph)" - - # Linker - void* p_cuLinkDestroy "reinterpret_cast(cuda_core::rt::p_cuLinkDestroy)" - - # Graphics interop - void* p_cuGraphicsUnmapResources "reinterpret_cast(cuda_core::rt::p_cuGraphicsUnmapResources)" - void* p_cuGraphicsUnregisterResource "reinterpret_cast(cuda_core::rt::p_cuGraphicsUnregisterResource)" - - # Texture / surface / array (PR #467) - void* p_cuArray3DCreate "reinterpret_cast(cuda_core::rt::p_cuArray3DCreate)" - void* p_cuArrayDestroy "reinterpret_cast(cuda_core::rt::p_cuArrayDestroy)" - void* p_cuMipmappedArrayCreate "reinterpret_cast(cuda_core::rt::p_cuMipmappedArrayCreate)" - void* p_cuMipmappedArrayDestroy "reinterpret_cast(cuda_core::rt::p_cuMipmappedArrayDestroy)" - void* p_cuMipmappedArrayGetLevel "reinterpret_cast(cuda_core::rt::p_cuMipmappedArrayGetLevel)" - void* p_cuTexObjectCreate "reinterpret_cast(cuda_core::rt::p_cuTexObjectCreate)" - void* p_cuTexObjectDestroy "reinterpret_cast(cuda_core::rt::p_cuTexObjectDestroy)" - void* p_cuSurfObjectCreate "reinterpret_cast(cuda_core::rt::p_cuSurfObjectCreate)" - void* p_cuSurfObjectDestroy "reinterpret_cast(cuda_core::rt::p_cuSurfObjectDestroy)" - - # SM resource split (13.1+) - void* p_cuDevSmResourceSplit "reinterpret_cast(cuda_core::rt::p_cuDevSmResourceSplit)" - - # cuMemcpyWithAttributesAsync (13.2+) - void* p_cuMemcpyWithAttributesAsync "reinterpret_cast(cuda_core::rt::p_cuMemcpyWithAttributesAsync)" - - # NVRTC - void* p_nvrtcDestroyProgram "reinterpret_cast(cuda_core::rt::p_nvrtcDestroyProgram)" - - # NVVM - void* p_nvvmDestroyProgram "reinterpret_cast(cuda_core::rt::p_nvvmDestroyProgram)" - - # nvJitLink - void* p_nvJitLinkDestroy "reinterpret_cast(cuda_core::rt::p_nvJitLinkDestroy)" - - -# Initialize driver function pointers from cydriver.__pyx_capi__ at module load -cdef void* _get_driver_fn(str name): - capsule = cydriver.__pyx_capi__[name] - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - - -cdef void* _get_optional_driver_fn(str name): - try: - capsule = cydriver.__pyx_capi__[name] - except KeyError: - return NULL - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - - -cdef void _init_driver_fn_pointers() noexcept: - global p_cuGetErrorName, p_cuGetErrorString - global p_cuDevicePrimaryCtxRetain, p_cuDevicePrimaryCtxRelease, p_cuCtxGetCurrent - global p_cuCtxSetCurrent, p_cuCtxSynchronize, p_cuCtxGetStreamPriorityRange - global p_cuCtxGetDevice, p_cuGraphNodeSetParams - global p_cuGreenCtxCreate, p_cuGreenCtxDestroy, p_cuCtxFromGreenCtx - global p_cuDevResourceGenerateDesc, p_cuGreenCtxStreamCreate - global p_cuStreamCreateWithPriority, p_cuStreamDestroy, p_cuStreamGetCtx - global p_cuEventCreate, p_cuEventDestroy, p_cuIpcOpenEventHandle - global p_cuDeviceGetCount - global p_cuMemPoolSetAccess, p_cuMemPoolDestroy, p_cuMemPoolCreate - global p_cuDeviceGetMemPool, p_cuMemPoolImportFromShareableHandle - global p_cuMemAllocFromPoolAsync, p_cuMemAllocAsync, p_cuMemAlloc, p_cuMemAllocHost - global p_cuMemFreeAsync, p_cuMemFree, p_cuMemFreeHost - global p_cuMemPoolImportPointer - global p_cuLibraryLoadFromFile, p_cuLibraryLoadData, p_cuLibraryUnload, p_cuLibraryGetKernel - global p_cuGraphDestroy, p_cuGraphInstantiateWithParams - global p_cuGraphExecUpdate, p_cuGraphExecDestroy - global p_cuUserObjectCreate, p_cuUserObjectRelease - global p_cuGraphRetainUserObject, p_cuGraphReleaseUserObject - global p_cuGraphNodeFindInClone, p_cuGraphChildGraphNodeGetGraph - global p_cuLinkDestroy - global p_cuGraphicsUnmapResources, p_cuGraphicsUnregisterResource - global p_cuDevSmResourceSplit - global p_cuMemcpyWithAttributesAsync - global p_cuArray3DCreate, p_cuArrayDestroy - global p_cuMipmappedArrayCreate, p_cuMipmappedArrayDestroy, p_cuMipmappedArrayGetLevel - global p_cuTexObjectCreate, p_cuTexObjectDestroy - global p_cuSurfObjectCreate, p_cuSurfObjectDestroy - - # Error formatting - p_cuGetErrorName = _get_driver_fn("cuGetErrorName") - p_cuGetErrorString = _get_driver_fn("cuGetErrorString") - - # Context - p_cuDevicePrimaryCtxRetain = _get_driver_fn("cuDevicePrimaryCtxRetain") - p_cuDevicePrimaryCtxRelease = _get_driver_fn("cuDevicePrimaryCtxRelease") - p_cuCtxGetCurrent = _get_driver_fn("cuCtxGetCurrent") - p_cuCtxSetCurrent = _get_driver_fn("cuCtxSetCurrent") - p_cuCtxSynchronize = _get_driver_fn("cuCtxSynchronize") - p_cuCtxGetStreamPriorityRange = _get_driver_fn("cuCtxGetStreamPriorityRange") - p_cuCtxGetDevice = _get_driver_fn("cuCtxGetDevice") - # Graph node parameter updates need CUDA 12.2+ (checked again at the call site). - p_cuGraphNodeSetParams = _get_optional_driver_fn("cuGraphNodeSetParams") - p_cuGreenCtxCreate = _get_optional_driver_fn("cuGreenCtxCreate") - p_cuGreenCtxDestroy = _get_optional_driver_fn("cuGreenCtxDestroy") - p_cuCtxFromGreenCtx = _get_optional_driver_fn("cuCtxFromGreenCtx") - p_cuDevResourceGenerateDesc = _get_optional_driver_fn("cuDevResourceGenerateDesc") - p_cuGreenCtxStreamCreate = _get_optional_driver_fn("cuGreenCtxStreamCreate") - - # Stream - p_cuStreamCreateWithPriority = _get_driver_fn("cuStreamCreateWithPriority") - p_cuStreamDestroy = _get_driver_fn("cuStreamDestroy") - p_cuStreamGetCtx = _get_driver_fn("cuStreamGetCtx") - - # Event - p_cuEventCreate = _get_driver_fn("cuEventCreate") - p_cuEventDestroy = _get_driver_fn("cuEventDestroy") - p_cuIpcOpenEventHandle = _get_driver_fn("cuIpcOpenEventHandle") - - # Device - p_cuDeviceGetCount = _get_driver_fn("cuDeviceGetCount") - - # Memory pool - p_cuMemPoolSetAccess = _get_driver_fn("cuMemPoolSetAccess") - p_cuMemPoolDestroy = _get_driver_fn("cuMemPoolDestroy") - p_cuMemPoolCreate = _get_driver_fn("cuMemPoolCreate") - p_cuDeviceGetMemPool = _get_driver_fn("cuDeviceGetMemPool") - p_cuMemPoolImportFromShareableHandle = _get_driver_fn("cuMemPoolImportFromShareableHandle") - - # Memory allocation - p_cuMemAllocFromPoolAsync = _get_driver_fn("cuMemAllocFromPoolAsync") - p_cuMemAllocAsync = _get_driver_fn("cuMemAllocAsync") - p_cuMemAlloc = _get_driver_fn("cuMemAlloc") - p_cuMemAllocHost = _get_driver_fn("cuMemAllocHost") - - # Memory deallocation - p_cuMemFreeAsync = _get_driver_fn("cuMemFreeAsync") - p_cuMemFree = _get_driver_fn("cuMemFree") - p_cuMemFreeHost = _get_driver_fn("cuMemFreeHost") - - # IPC - p_cuMemPoolImportPointer = _get_driver_fn("cuMemPoolImportPointer") - - # Library - p_cuLibraryLoadFromFile = _get_driver_fn("cuLibraryLoadFromFile") - p_cuLibraryLoadData = _get_driver_fn("cuLibraryLoadData") - p_cuLibraryUnload = _get_driver_fn("cuLibraryUnload") - p_cuLibraryGetKernel = _get_driver_fn("cuLibraryGetKernel") - - # Graph - p_cuGraphDestroy = _get_driver_fn("cuGraphDestroy") - p_cuGraphInstantiateWithParams = _get_driver_fn("cuGraphInstantiateWithParams") - p_cuGraphExecUpdate = _get_driver_fn("cuGraphExecUpdate") - p_cuGraphExecDestroy = _get_driver_fn("cuGraphExecDestroy") - p_cuUserObjectCreate = _get_driver_fn("cuUserObjectCreate") - p_cuUserObjectRelease = _get_driver_fn("cuUserObjectRelease") - p_cuGraphRetainUserObject = _get_driver_fn("cuGraphRetainUserObject") - p_cuGraphReleaseUserObject = _get_driver_fn("cuGraphReleaseUserObject") - p_cuGraphNodeFindInClone = _get_driver_fn("cuGraphNodeFindInClone") - p_cuGraphChildGraphNodeGetGraph = _get_driver_fn("cuGraphChildGraphNodeGetGraph") - - # Linker - p_cuLinkDestroy = _get_driver_fn("cuLinkDestroy") - - # Graphics interop - p_cuGraphicsUnmapResources = _get_driver_fn("cuGraphicsUnmapResources") - p_cuGraphicsUnregisterResource = _get_driver_fn("cuGraphicsUnregisterResource") - - # Texture / surface / array (PR #467) - p_cuArray3DCreate = _get_driver_fn("cuArray3DCreate") - p_cuArrayDestroy = _get_driver_fn("cuArrayDestroy") - p_cuMipmappedArrayCreate = _get_driver_fn("cuMipmappedArrayCreate") - p_cuMipmappedArrayDestroy = _get_driver_fn("cuMipmappedArrayDestroy") - p_cuMipmappedArrayGetLevel = _get_driver_fn("cuMipmappedArrayGetLevel") - p_cuTexObjectCreate = _get_driver_fn("cuTexObjectCreate") - p_cuTexObjectDestroy = _get_driver_fn("cuTexObjectDestroy") - p_cuSurfObjectCreate = _get_driver_fn("cuSurfObjectCreate") - p_cuSurfObjectDestroy = _get_driver_fn("cuSurfObjectDestroy") - - # SM resource split (13.1+ — may not exist in older cuda-bindings) - p_cuDevSmResourceSplit = _get_optional_driver_fn("cuDevSmResourceSplit") - - # cuMemcpyWithAttributesAsync (13.2+ — may not exist in older cuda-bindings) - p_cuMemcpyWithAttributesAsync = _get_optional_driver_fn("cuMemcpyWithAttributesAsync") - -_init_driver_fn_pointers() initialize_deferred_cleanup() @@ -599,51 +318,3 @@ def _attach_rollback_failure_for_testing(int status): """ _attach_rollback_failure_local( b"cuTestOperation", status, b"failed while testing") - -# ============================================================================= -# NVRTC function pointer initialization -# ============================================================================= - -cdef void* _get_nvrtc_fn(str name): - capsule = cynvrtc.__pyx_capi__[name] - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - -cdef void _init_nvrtc_fn_pointers() noexcept: - global p_nvrtcDestroyProgram - p_nvrtcDestroyProgram = _get_nvrtc_fn("nvrtcDestroyProgram") - -_init_nvrtc_fn_pointers() - -# ============================================================================= -# NVVM function pointer initialization -# -# NVVM may not be available at runtime, so we handle missing function pointers -# gracefully. The C++ deleter checks for null before calling. -# ============================================================================= - -cdef void* _get_nvvm_fn(str name): - capsule = cynvvm.__pyx_capi__[name] - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - -cdef void _init_nvvm_fn_pointers() noexcept: - global p_nvvmDestroyProgram - p_nvvmDestroyProgram = _get_nvvm_fn("nvvmDestroyProgram") - -_init_nvvm_fn_pointers() - -# ============================================================================= -# nvJitLink function pointer initialization -# -# nvJitLink may not be available at runtime, so we handle missing function -# pointers gracefully. The C++ deleter checks for null before calling. -# ============================================================================= - -cdef void* _get_nvjitlink_fn(str name): - capsule = cynvjitlink.__pyx_capi__[name] - return PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule)) - -cdef void _init_nvjitlink_fn_pointers() noexcept: - global p_nvJitLinkDestroy - p_nvJitLinkDestroy = _get_nvjitlink_fn("nvJitLinkDestroy") - -_init_nvjitlink_fn_pointers() diff --git a/cuda_core/cuda/core/_stream.pyx b/cuda_core/cuda/core/_stream.pyx index 4c51b32f4f0..2ed1e402be4 100644 --- a/cuda_core/cuda/core/_stream.pyx +++ b/cuda_core/cuda/core/_stream.pyx @@ -14,6 +14,7 @@ from cuda.core._utils.cuda_utils cimport ( check_or_create_options, HANDLE_RETURN, ) +from cuda.core._utils.version cimport cy_driver_version import cython import warnings @@ -171,7 +172,14 @@ cdef class Stream: prio = high # C++ creates the stream and returns owning handle with context dependency. - # For green contexts, the C++ layer auto-dispatches to cuGreenCtxStreamCreate. + # For green contexts, the C++ layer auto-dispatches to cuGreenCtxStreamCreate, + # a 12.5 driver API (cuGreenCtxCreate itself is 12.4); the driver alone + # decides availability, and the gate lives here, not in C++. + if context.is_green and cy_driver_version() < (12, 5, 0): + raise RuntimeError( + "Green context stream creation requires CUDA driver 12.5 or newer " + f"(current driver: {'.'.join(map(str, cy_driver_version()))})" + ) h_stream = create_stream_handle(h_context, flags, prio) if not h_stream: res_code = get_last_error() @@ -182,11 +190,6 @@ cdef class Stream: "Green context streams must be non-blocking. " "Use StreamOptions(nonblocking=True) or omit the option (True is the default)." ) - elif res_code == cydriver.CUresult.CUDA_ERROR_NOT_SUPPORTED: - raise RuntimeError( - "cuGreenCtxStreamCreate is not available. " - "Green context stream creation requires CUDA 12.5 or newer." - ) else: HANDLE_RETURN(res_code) cdef Stream self = Stream._from_handle(cls, h_stream) diff --git a/cuda_core/cuda/core/_utils/driver_cu_result_explanations.py b/cuda_core/cuda/core/_utils/driver_cu_result_explanations.py index d1e0a53bb00..a8c9f0e9b69 100644 --- a/cuda_core/cuda/core/_utils/driver_cu_result_explanations.py +++ b/cuda_core/cuda/core/_utils/driver_cu_result_explanations.py @@ -4,13 +4,6 @@ from __future__ import annotations from cuda.bindings import driver -from cuda.core._utils.enum_explanations_helpers import get_best_available_explanations +from cuda.core._utils.enum_explanations_helpers import DocstringBackedExplanations - -def _load_fallback_explanations() -> dict[int, str | tuple[str, ...]]: - from cuda.core._utils.driver_cu_result_explanations_frozen import _FALLBACK_EXPLANATIONS - - return _FALLBACK_EXPLANATIONS # type: ignore[return-value] - - -DRIVER_CU_RESULT_EXPLANATIONS = get_best_available_explanations(driver.CUresult, _load_fallback_explanations) +DRIVER_CU_RESULT_EXPLANATIONS = DocstringBackedExplanations(driver.CUresult) diff --git a/cuda_core/cuda/core/_utils/driver_cu_result_explanations_frozen.py b/cuda_core/cuda/core/_utils/driver_cu_result_explanations_frozen.py deleted file mode 100644 index 41eb158f4e2..00000000000 --- a/cuda_core/cuda/core/_utils/driver_cu_result_explanations_frozen.py +++ /dev/null @@ -1,357 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Like the runtime counterpart, this fallback is a deliberately frozen -# compatibility snapshot, not a release-maintained mirror of CUDA's enums. -# Do not update it past CUDA Toolkit v13.1.1. Bindings releases new enough to -# define later codes provide explanations through enum-member docstrings; if an -# older binding receives one from a newer driver, it falls through to -# cuGetErrorString(). Synchronizing this table with later Toolkit releases would -# restore the duplicate maintenance burden removed by PR #1860. -# CUDA Toolkit v13.1.1 -_FALLBACK_EXPLANATIONS = { - 0: ( - "The API call returned with no errors. In the case of query calls, this" - " also means that the operation being queried is complete (see" - " ::cuEventQuery() and ::cuStreamQuery())." - ), - 1: ( - "This indicates that one or more of the parameters passed to the API call" - " is not within an acceptable range of values." - ), - 2: ( - "The API call failed because it was unable to allocate enough memory or" - " other resources to perform the requested operation." - ), - 3: ( - "This indicates that the CUDA driver has not been initialized with" - " ::cuInit() or that initialization has failed." - ), - 4: "This indicates that the CUDA driver is in the process of shutting down.", - 5: ( - "This indicates profiler is not initialized for this run. This can" - " happen when the application is running with external profiling tools" - " like visual profiler." - ), - 6: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to attempt to enable/disable the profiling via ::cuProfilerStart or" - " ::cuProfilerStop without initialization." - ), - 7: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to call cuProfilerStart() when profiling is already enabled." - ), - 8: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to call cuProfilerStop() when profiling is already disabled." - ), - 34: ( - "This indicates that the CUDA driver that the application has loaded is a" - " stub library. Applications that run with the stub rather than a real" - " driver loaded will result in CUDA API returning this error." - ), - 36: ( - "This indicates that the API call requires a newer CUDA driver than the one" - " currently installed. Users should install an updated NVIDIA CUDA driver" - " to allow the API call to succeed." - ), - 46: ( - "This indicates that requested CUDA device is unavailable at the current" - " time. Devices are often unavailable due to use of" - " ::CU_COMPUTEMODE_EXCLUSIVE_PROCESS or ::CU_COMPUTEMODE_PROHIBITED." - ), - 100: ("This indicates that no CUDA-capable devices were detected by the installed CUDA driver."), - 101: ( - "This indicates that the device ordinal supplied by the user does not" - " correspond to a valid CUDA device or that the action requested is" - " invalid for the specified device." - ), - 102: "This error indicates that the Grid license is not applied.", - 200: ("This indicates that the device kernel image is invalid. This can also indicate an invalid CUDA module."), - 201: ( - "This most frequently indicates that there is no context bound to the" - " current thread. This can also be returned if the context passed to an" - " API call is not a valid handle (such as a context that has had" - " ::cuCtxDestroy() invoked on it). This can also be returned if a user" - " mixes different API versions (i.e. 3010 context with 3020 API calls)." - " See ::cuCtxGetApiVersion() for more details." - " This can also be returned if the green context passed to an API call" - " was not converted to a ::CUcontext using ::cuCtxFromGreenCtx API." - ), - 202: ( - "This indicated that the context being supplied as a parameter to the" - " API call was already the active context." - " This error return is deprecated as of CUDA 3.2. It is no longer an" - " error to attempt to push the active context via ::cuCtxPushCurrent()." - ), - 205: "This indicates that a map or register operation has failed.", - 206: "This indicates that an unmap or unregister operation has failed.", - 207: ("This indicates that the specified array is currently mapped and thus cannot be destroyed."), - 208: "This indicates that the resource is already mapped.", - 209: ( - "This indicates that there is no kernel image available that is suitable" - " for the device. This can occur when a user specifies code generation" - " options for a particular CUDA source file that do not include the" - " corresponding device configuration." - ), - 210: "This indicates that a resource has already been acquired.", - 211: "This indicates that a resource is not mapped.", - 212: ("This indicates that a mapped resource is not available for access as an array."), - 213: ("This indicates that a mapped resource is not available for access as a pointer."), - 214: ("This indicates that an uncorrectable ECC error was detected during execution."), - 215: ("This indicates that the ::CUlimit passed to the API call is not supported by the active device."), - 216: ( - "This indicates that the ::CUcontext passed to the API call can" - " only be bound to a single CPU thread at a time but is already" - " bound to a CPU thread." - ), - 217: ("This indicates that peer access is not supported across the given devices."), - 218: "This indicates that a PTX JIT compilation failed.", - 219: "This indicates an error with OpenGL or DirectX context.", - 220: ("This indicates that an uncorrectable NVLink error was detected during the execution."), - 221: "This indicates that the PTX JIT compiler library was not found.", - 222: "This indicates that the provided PTX was compiled with an unsupported toolchain.", - 223: "This indicates that the PTX JIT compilation was disabled.", - 224: ("This indicates that the ::CUexecAffinityType passed to the API call is not supported by the active device."), - 225: ( - "This indicates that the code to be compiled by the PTX JIT contains unsupported call to cudaDeviceSynchronize." - ), - 226: ( - "This indicates that an exception occurred on the device that is now" - " contained by the GPU's error containment capability. Common causes are -" - " a. Certain types of invalid accesses of peer GPU memory over nvlink" - " b. Certain classes of hardware errors" - " This leaves the process in an inconsistent state and any further CUDA" - " work will return the same error. To continue using CUDA, the process must" - " be terminated and relaunched." - ), - 300: ( - "This indicates that the device kernel source is invalid. This includes" - " compilation/linker errors encountered in device code or user error." - ), - 301: "This indicates that the file specified was not found.", - 302: "This indicates that a link to a shared object failed to resolve.", - 303: "This indicates that initialization of a shared object failed.", - 304: "This indicates that an OS call failed.", - 400: ( - "This indicates that a resource handle passed to the API call was not" - " valid. Resource handles are opaque types like ::CUstream and ::CUevent." - ), - 401: ( - "This indicates that a resource required by the API call is not in a" - " valid state to perform the requested operation." - ), - 402: ( - "This indicates an attempt was made to introspect an object in a way that" - " would discard semantically important information. This is either due to" - " the object using funtionality newer than the API version used to" - " introspect it or omission of optional return arguments." - ), - 500: ( - "This indicates that a named symbol was not found. Examples of symbols" - " are global/constant variable names, driver function names, texture names," - " and surface names." - ), - 600: ( - "This indicates that asynchronous operations issued previously have not" - " completed yet. This result is not actually an error, but must be indicated" - " differently than ::CUDA_SUCCESS (which indicates completion). Calls that" - " may return this value include ::cuEventQuery() and ::cuStreamQuery()." - ), - 700: ( - "While executing a kernel, the device encountered a" - " load or store instruction on an invalid memory address." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 701: ( - "This indicates that a launch did not occur because it did not have" - " appropriate resources. This error usually indicates that the user has" - " attempted to pass too many arguments to the device kernel, or the" - " kernel launch specifies too many threads for the kernel's register" - " count. Passing arguments of the wrong size (i.e. a 64-bit pointer" - " when a 32-bit int is expected) is equivalent to passing too many" - " arguments and can also result in this error." - ), - 702: ( - "This indicates that the device kernel took too long to execute. This can" - " only occur if timeouts are enabled - see the device attribute" - " ::CU_DEVICE_ATTRIBUTE_KERNEL_EXEC_TIMEOUT for more information." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 703: ("This error indicates a kernel launch that uses an incompatible texturing mode."), - 704: ( - "This error indicates that a call to ::cuCtxEnablePeerAccess() is" - " trying to re-enable peer access to a context which has already" - " had peer access to it enabled." - ), - 705: ( - "This error indicates that ::cuCtxDisablePeerAccess() is" - " trying to disable peer access which has not been enabled yet" - " via ::cuCtxEnablePeerAccess()." - ), - 708: ("This error indicates that the primary context for the specified device has already been initialized."), - 709: ( - "This error indicates that the context current to the calling thread" - " has been destroyed using ::cuCtxDestroy, or is a primary context which" - " has not yet been initialized." - ), - 710: ( - "A device-side assert triggered during kernel execution. The context" - " cannot be used anymore, and must be destroyed. All existing device" - " memory allocations from this context are invalid and must be" - " reconstructed if the program is to continue using CUDA." - ), - 711: ( - "This error indicates that the hardware resources required to enable" - " peer access have been exhausted for one or more of the devices" - " passed to ::cuCtxEnablePeerAccess()." - ), - 712: ("This error indicates that the memory range passed to ::cuMemHostRegister() has already been registered."), - 713: ( - "This error indicates that the pointer passed to ::cuMemHostUnregister()" - " does not correspond to any currently registered memory region." - ), - 714: ( - "While executing a kernel, the device encountered a stack error." - " This can be due to stack corruption or exceeding the stack size limit." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 715: ( - "While executing a kernel, the device encountered an illegal instruction." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 716: ( - "While executing a kernel, the device encountered a load or store instruction" - " on a memory address which is not aligned." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 717: ( - "While executing a kernel, the device encountered an instruction" - " which can only operate on memory locations in certain address spaces" - " (global, shared, or local), but was supplied a memory address not" - " belonging to an allowed address space." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 718: ( - "While executing a kernel, the device program counter wrapped its address space." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 719: ( - "An exception occurred on the device while executing a kernel. Common" - " causes include dereferencing an invalid device pointer and accessing" - " out of bounds shared memory. Less common cases can be system specific - more" - " information about these cases can be found in the system specific user guide." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 720: ( - "This error indicates that the number of blocks launched per grid for a kernel that was" - " launched via either ::cuLaunchCooperativeKernel or ::cuLaunchCooperativeKernelMultiDevice" - " exceeds the maximum number of blocks as allowed by ::cuOccupancyMaxActiveBlocksPerMultiprocessor" - " or ::cuOccupancyMaxActiveBlocksPerMultiprocessorWithFlags times the number of multiprocessors" - " as specified by the device attribute ::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT." - ), - 721: ( - "An exception occurred on the device while exiting a kernel using tensor memory: the" - " tensor memory was not completely deallocated. This leaves the process in an inconsistent" - " state and any further CUDA work will return the same error. To continue using CUDA, the" - " process must be terminated and relaunched." - ), - 800: "This error indicates that the attempted operation is not permitted.", - 801: ("This error indicates that the attempted operation is not supported on the current system or device."), - 802: ( - "This error indicates that the system is not yet ready to start any CUDA" - " work. To continue using CUDA, verify the system configuration is in a" - " valid state and all required driver daemons are actively running." - " More information about this error can be found in the system specific" - " user guide." - ), - 803: ( - "This error indicates that there is a mismatch between the versions of" - " the display driver and the CUDA driver. Refer to the compatibility documentation" - " for supported versions." - ), - 804: ( - "This error indicates that the system was upgraded to run with forward compatibility" - " but the visible hardware detected by CUDA does not support this configuration." - " Refer to the compatibility documentation for the supported hardware matrix or ensure" - " that only supported hardware is visible during initialization via the CUDA_VISIBLE_DEVICES" - " environment variable." - ), - 805: "This error indicates that the MPS client failed to connect to the MPS control daemon or the MPS server.", - 806: "This error indicates that the remote procedural call between the MPS server and the MPS client failed.", - 807: ( - "This error indicates that the MPS server is not ready to accept new MPS client requests." - " This error can be returned when the MPS server is in the process of recovering from a fatal failure." - ), - 808: "This error indicates that the hardware resources required to create MPS client have been exhausted.", - 809: "This error indicates the the hardware resources required to support device connections have been exhausted.", - 810: "This error indicates that the MPS client has been terminated by the server. To continue using CUDA, the process must be terminated and relaunched.", - 811: "This error indicates that the module is using CUDA Dynamic Parallelism, but the current configuration, like MPS, does not support it.", - 812: "This error indicates that a module contains an unsupported interaction between different versions of CUDA Dynamic Parallelism.", - 900: ("This error indicates that the operation is not permitted when the stream is capturing."), - 901: ( - "This error indicates that the current capture sequence on the stream" - " has been invalidated due to a previous error." - ), - 902: ( - "This error indicates that the operation would have resulted in a merge of two independent capture sequences." - ), - 903: "This error indicates that the capture was not initiated in this stream.", - 904: ("This error indicates that the capture sequence contains a fork that was not joined to the primary stream."), - 905: ( - "This error indicates that a dependency would have been created which" - " crosses the capture sequence boundary. Only implicit in-stream ordering" - " dependencies are allowed to cross the boundary." - ), - 906: ("This error indicates a disallowed implicit dependency on a current capture sequence from cudaStreamLegacy."), - 907: ( - "This error indicates that the operation is not permitted on an event which" - " was last recorded in a capturing stream." - ), - 908: ( - "A stream capture sequence not initiated with the ::CU_STREAM_CAPTURE_MODE_RELAXED" - " argument to ::cuStreamBeginCapture was passed to ::cuStreamEndCapture in a" - " different thread." - ), - 909: "This error indicates that the timeout specified for the wait operation has lapsed.", - 910: ( - "This error indicates that the graph update was not performed because it included" - " changes which violated constraints specific to instantiated graph update." - ), - 911: ( - "This indicates that an async error has occurred in a device outside of CUDA." - " If CUDA was waiting for an external device's signal before consuming shared data," - " the external device signaled an error indicating that the data is not valid for" - " consumption. This leaves the process in an inconsistent state and any further CUDA" - " work will return the same error. To continue using CUDA, the process must be" - " terminated and relaunched." - ), - 912: "Indicates a kernel launch error due to cluster misconfiguration.", - 913: ("Indiciates a function handle is not loaded when calling an API that requires a loaded function."), - 914: ("This error indicates one or more resources passed in are not valid resource types for the operation."), - 915: ("This error indicates one or more resources are insufficient or non-applicable for the operation."), - 916: ("This error indicates that an error happened during the key rotation sequence."), - 917: ( - "This error indicates that the requested operation is not permitted because the" - " stream is in a detached state. This can occur if the green context associated" - " with the stream has been destroyed, limiting the stream's operational capabilities." - ), - 999: "This indicates that an unknown internal error has occurred.", -} diff --git a/cuda_core/cuda/core/_utils/enum_explanations_helpers.py b/cuda_core/cuda/core/_utils/enum_explanations_helpers.py index 6b666f4536c..e5e46145a83 100644 --- a/cuda_core/cuda/core/_utils/enum_explanations_helpers.py +++ b/cuda_core/cuda/core/_utils/enum_explanations_helpers.py @@ -3,62 +3,24 @@ """Internal support for error-enum explanations. -``cuda_core`` keeps frozen 13.1.1 fallback tables for older ``cuda-bindings`` -releases. Driver/runtime error enums carry usable ``__doc__`` text starting in -the 12.x backport line at ``cuda-bindings`` 12.9.6, and in the mainline 13.x -series at ``cuda-bindings`` 13.2.0. This module decides which source to use -and normalizes generated docstrings so user-facing ``CUDAError`` messages stay -presentable. +Driver and runtime error enums in ``cuda-bindings`` carry per-member +``__doc__`` text (since 12.9.6 in the 12.x line and 13.2.0 in the 13.x line; +every ``cuda-bindings`` that ``cuda.core`` accepts has it). This module +normalizes those generated docstrings so user-facing ``CUDAError`` messages +stay presentable. The cleanup rules here were derived while validating generated enum docstrings -in PR #1805. Keep them narrow and remove them when codegen quirks or fallback -support are no longer needed. +in PR #1805. Keep them narrow and remove them when the codegen quirks are gone. """ from __future__ import annotations -import importlib.metadata import re -from collections.abc import Callable from typing import Any -_MIN_12X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS = (12, 9, 6) -_MIN_13X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS = (13, 2, 0) _RST_INLINE_ROLE_RE = re.compile(r":(?:[a-z]+:)?[a-z]+:`([^`]+)`") _WORDWRAP_HYPHEN_AFTER_RE = re.compile(r"(?<=[0-9A-Za-z_])- (?=[0-9A-Za-z_])") _WORDWRAP_HYPHEN_BEFORE_RE = re.compile(r"(?<=[0-9A-Za-z_]) -(?=[0-9A-Za-z_])") -_ExplanationTable = dict[int, str | tuple[str, ...]] -_ExplanationTableLoader = Callable[[], _ExplanationTable] - - -def _parse_version_triple(version_str: str) -> tuple[int, int, int]: - """Parse a PEP 440 version string into a (major, minor, patch) triple. - - Strips local-version identifiers and handles pre-release suffixes such as - ``0b1`` or ``0rc1`` by extracting only the leading integer from each - release segment. - """ - parts = version_str.partition("+")[0].split(".")[:3] - ints = ([int(m.group(1)) if (m := re.match(r"(\d+)", v)) else 0 for v in parts] + [0, 0, 0])[:3] - return (ints[0], ints[1], ints[2]) - - -# ``version.pyx`` cannot be reused here (circular import via ``cuda_utils``). -def _binding_version() -> tuple[int, int, int]: - """Return the installed ``cuda-bindings`` version, or a conservative old value.""" - try: - version = importlib.metadata.version("cuda-bindings") - except importlib.metadata.PackageNotFoundError: - return (0, 0, 0) # For very old versions of cuda-python - return _parse_version_triple(version) - - -def _binding_version_has_usable_enum_docstrings(version: tuple[int, int, int]) -> bool: - """Whether released bindings are known to carry usable error-enum ``__doc__`` text.""" - return ( - _MIN_12X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS <= version < (13, 0, 0) - or version >= _MIN_13X_BINDING_VERSION_FOR_ENUM_DOCSTRINGS - ) def _fix_hyphenation_wordwrap_spacing(s: str) -> str: @@ -102,9 +64,9 @@ def clean_enum_member_docstring(doc: str | None) -> str | None: class DocstringBackedExplanations: - """Compatibility shim exposing enum-member ``__doc__`` text via ``dict.get``. + """Expose enum-member ``__doc__`` text via ``dict.get``. - Keeps the existing ``.get(int(error))`` lookup shape used by ``cuda_utils.pyx``. + Keeps the ``.get(int(error))`` lookup shape used by ``cuda_utils.pyx``. """ __slots__ = ("_enum_type",) @@ -123,20 +85,3 @@ def get(self, code: int, default: str | None = None) -> str | None: return default return clean_enum_member_docstring(raw_doc) - - -def get_best_available_explanations( - enum_type: Any, - fallback: _ExplanationTable | _ExplanationTableLoader, -) -> DocstringBackedExplanations | _ExplanationTable: - """Pick one explanation source per bindings version. - - Use enum-member ``__doc__`` only for bindings versions known to expose - usable per-member text (12.9.6+ in the 12.x backport line, 13.2.0+ in the - 13.x mainline). Otherwise keep using the frozen 13.1.1 fallback tables. - """ - if not _binding_version_has_usable_enum_docstrings(_binding_version()): - if callable(fallback): - return fallback() - return fallback - return DocstringBackedExplanations(enum_type) diff --git a/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations.py b/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations.py index 12a49d2ec96..3bff19207e1 100644 --- a/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations.py +++ b/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations.py @@ -4,13 +4,6 @@ from __future__ import annotations from cuda.bindings import runtime -from cuda.core._utils.enum_explanations_helpers import get_best_available_explanations +from cuda.core._utils.enum_explanations_helpers import DocstringBackedExplanations - -def _load_fallback_explanations() -> dict[int, str | tuple[str, ...]]: - from cuda.core._utils.runtime_cuda_error_explanations_frozen import _FALLBACK_EXPLANATIONS - - return _FALLBACK_EXPLANATIONS # type: ignore[return-value] - - -RUNTIME_CUDA_ERROR_EXPLANATIONS = get_best_available_explanations(runtime.cudaError_t, _load_fallback_explanations) +RUNTIME_CUDA_ERROR_EXPLANATIONS = DocstringBackedExplanations(runtime.cudaError_t) diff --git a/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations_frozen.py b/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations_frozen.py deleted file mode 100644 index 017c4087400..00000000000 --- a/cuda_core/cuda/core/_utils/runtime_cuda_error_explanations_frozen.py +++ /dev/null @@ -1,538 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# CUDA Toolkit v13.1.1 -_FALLBACK_EXPLANATIONS = { - 0: ( - "The API call returned with no errors. In the case of query calls, this" - " also means that the operation being queried is complete (see" - " ::cudaEventQuery() and ::cudaStreamQuery())." - ), - 1: ( - "This indicates that one or more of the parameters passed to the API call" - " is not within an acceptable range of values." - ), - 2: ( - "The API call failed because it was unable to allocate enough memory or" - " other resources to perform the requested operation." - ), - 3: ("The API call failed because the CUDA driver and runtime could not be initialized."), - 4: ( - "This indicates that a CUDA Runtime API call cannot be executed because" - " it is being called during process shut down, at a point in time after" - " CUDA driver has been unloaded." - ), - 5: ( - "This indicates profiler is not initialized for this run. This can" - " happen when the application is running with external profiling tools" - " like visual profiler." - ), - 6: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to attempt to enable/disable the profiling via ::cudaProfilerStart or" - " ::cudaProfilerStop without initialization." - ), - 7: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to call cudaProfilerStart() when profiling is already enabled." - ), - 8: ( - "This error return is deprecated as of CUDA 5.0. It is no longer an error" - " to call cudaProfilerStop() when profiling is already disabled." - ), - 9: ( - "This indicates that a kernel launch is requesting resources that can" - " never be satisfied by the current device. Requesting more shared memory" - " per block than the device supports will trigger this error, as will" - " requesting too many threads or blocks. See ::cudaDeviceProp for more" - " device limitations." - ), - 12: ( - "This indicates that one or more of the pitch-related parameters passed" - " to the API call is not within the acceptable range for pitch." - ), - 13: ("This indicates that the symbol name/identifier passed to the API call is not a valid name or identifier."), - 16: ( - "This indicates that at least one host pointer passed to the API call is" - " not a valid host pointer." - " This error return is deprecated as of CUDA 10.1." - ), - 17: ( - "This indicates that at least one device pointer passed to the API call is" - " not a valid device pointer." - " This error return is deprecated as of CUDA 10.1." - ), - 18: ("This indicates that the texture passed to the API call is not a valid texture."), - 19: ( - "This indicates that the texture binding is not valid. This occurs if you" - " call ::cudaGetTextureAlignmentOffset() with an unbound texture." - ), - 20: ( - "This indicates that the channel descriptor passed to the API call is not" - " valid. This occurs if the format is not one of the formats specified by" - " ::cudaChannelFormatKind, or if one of the dimensions is invalid." - ), - 21: ( - "This indicates that the direction of the memcpy passed to the API call is" - " not one of the types specified by ::cudaMemcpyKind." - ), - 22: ( - "This indicated that the user has taken the address of a constant variable," - " which was forbidden up until the CUDA 3.1 release." - " This error return is deprecated as of CUDA 3.1. Variables in constant" - " memory may now have their address taken by the runtime via" - " ::cudaGetSymbolAddress()." - ), - 23: ( - "This indicated that a texture fetch was not able to be performed." - " This was previously used for device emulation of texture operations." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 24: ( - "This indicated that a texture was not bound for access." - " This was previously used for device emulation of texture operations." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 25: ( - "This indicated that a synchronization operation had failed." - " This was previously used for some device emulation functions." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 26: ( - "This indicates that a non-float texture was being accessed with linear" - " filtering. This is not supported by CUDA." - ), - 27: ( - "This indicates that an attempt was made to read an unsupported data type as a" - " normalized float. This is not supported by CUDA." - ), - 28: ( - "Mixing of device and device emulation code was not allowed." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 31: ( - "This indicates that the API call is not yet implemented. Production" - " releases of CUDA will never return this error." - " This error return is deprecated as of CUDA 4.1." - ), - 32: ( - "This indicated that an emulated device pointer exceeded the 32-bit address" - " range." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 34: ( - "This indicates that the CUDA driver that the application has loaded is a" - " stub library. Applications that run with the stub rather than a real" - " driver loaded will result in CUDA API returning this error." - ), - 35: ( - "This indicates that the installed NVIDIA CUDA driver is older than the" - " CUDA runtime library. This is not a supported configuration. Users should" - " install an updated NVIDIA display driver to allow the application to run." - ), - 36: ( - "This indicates that the API call requires a newer CUDA driver than the one" - " currently installed. Users should install an updated NVIDIA CUDA driver" - " to allow the API call to succeed." - ), - 37: ("This indicates that the surface passed to the API call is not a valid surface."), - 43: ( - "This indicates that multiple global or constant variables (across separate" - " CUDA source files in the application) share the same string name." - ), - 44: ( - "This indicates that multiple textures (across separate CUDA source" - " files in the application) share the same string name." - ), - 45: ( - "This indicates that multiple surfaces (across separate CUDA source" - " files in the application) share the same string name." - ), - 46: ( - "This indicates that all CUDA devices are busy or unavailable at the current" - " time. Devices are often busy/unavailable due to use of" - " ::cudaComputeModeProhibited, ::cudaComputeModeExclusiveProcess, or when long" - " running CUDA kernels have filled up the GPU and are blocking new work" - " from starting. They can also be unavailable due to memory constraints" - " on a device that already has active CUDA work being performed." - ), - 49: ( - "This indicates that the current context is not compatible with this" - " the CUDA Runtime. This can only occur if you are using CUDA" - " Runtime/Driver interoperability and have created an existing Driver" - " context using the driver API. The Driver context may be incompatible" - " either because the Driver context was created using an older version" - " of the API, because the Runtime API call expects a primary driver" - " context and the Driver context is not primary, or because the Driver" - ' context has been destroyed. Please see CUDART_DRIVER "Interactions' - ' with the CUDA Driver API" for more information.' - ), - 52: ( - "The device function being invoked (usually via ::cudaLaunchKernel()) was not" - " previously configured via the ::cudaConfigureCall() function." - ), - 53: ( - "This indicated that a previous kernel launch failed. This was previously" - " used for device emulation of kernel launches." - " This error return is deprecated as of CUDA 3.1. Device emulation mode was" - " removed with the CUDA 3.1 release." - ), - 65: ( - "This error indicates that a device runtime grid launch did not occur" - " because the depth of the child grid would exceed the maximum supported" - " number of nested grid launches." - ), - 66: ( - "This error indicates that a grid launch did not occur because the kernel" - " uses file-scoped textures which are unsupported by the device runtime." - " Kernels launched via the device runtime only support textures created with" - " the Texture Object API's." - ), - 67: ( - "This error indicates that a grid launch did not occur because the kernel" - " uses file-scoped surfaces which are unsupported by the device runtime." - " Kernels launched via the device runtime only support surfaces created with" - " the Surface Object API's." - ), - 68: ( - "This error indicates that a call to ::cudaDeviceSynchronize made from" - " the device runtime failed because the call was made at grid depth greater" - " than than either the default (2 levels of grids) or user specified device" - " limit ::cudaLimitDevRuntimeSyncDepth. To be able to synchronize on" - " launched grids at a greater depth successfully, the maximum nested" - " depth at which ::cudaDeviceSynchronize will be called must be specified" - " with the ::cudaLimitDevRuntimeSyncDepth limit to the ::cudaDeviceSetLimit" - " api before the host-side launch of a kernel using the device runtime." - " Keep in mind that additional levels of sync depth require the runtime" - " to reserve large amounts of device memory that cannot be used for" - " user allocations. Note that ::cudaDeviceSynchronize made from device" - " runtime is only supported on devices of compute capability < 9.0." - ), - 69: ( - "This error indicates that a device runtime grid launch failed because" - " the launch would exceed the limit ::cudaLimitDevRuntimePendingLaunchCount." - " For this launch to proceed successfully, ::cudaDeviceSetLimit must be" - " called to set the ::cudaLimitDevRuntimePendingLaunchCount to be higher" - " than the upper bound of outstanding launches that can be issued to the" - " device runtime. Keep in mind that raising the limit of pending device" - " runtime launches will require the runtime to reserve device memory that" - " cannot be used for user allocations." - ), - 98: ("The requested device function does not exist or is not compiled for the proper device architecture."), - 100: ("This indicates that no CUDA-capable devices were detected by the installed CUDA driver."), - 101: ( - "This indicates that the device ordinal supplied by the user does not" - " correspond to a valid CUDA device or that the action requested is" - " invalid for the specified device." - ), - 102: "This indicates that the device doesn't have a valid Grid License.", - 103: ( - "By default, the CUDA runtime may perform a minimal set of self-tests," - " as well as CUDA driver tests, to establish the validity of both." - " Introduced in CUDA 11.2, this error return indicates that at least one" - " of these tests has failed and the validity of either the runtime" - " or the driver could not be established." - ), - 127: "This indicates an internal startup failure in the CUDA runtime.", - 200: "This indicates that the device kernel image is invalid.", - 201: ( - "This most frequently indicates that there is no context bound to the" - " current thread. This can also be returned if the context passed to an" - " API call is not a valid handle (such as a context that has had" - " ::cuCtxDestroy() invoked on it). This can also be returned if a user" - " mixes different API versions (i.e. 3010 context with 3020 API calls)." - " See ::cuCtxGetApiVersion() for more details." - ), - 205: "This indicates that the buffer object could not be mapped.", - 206: "This indicates that the buffer object could not be unmapped.", - 207: ("This indicates that the specified array is currently mapped and thus cannot be destroyed."), - 208: "This indicates that the resource is already mapped.", - 209: ( - "This indicates that there is no kernel image available that is suitable" - " for the device. This can occur when a user specifies code generation" - " options for a particular CUDA source file that do not include the" - " corresponding device configuration." - ), - 210: "This indicates that a resource has already been acquired.", - 211: "This indicates that a resource is not mapped.", - 212: ("This indicates that a mapped resource is not available for access as an array."), - 213: ("This indicates that a mapped resource is not available for access as a pointer."), - 214: ("This indicates that an uncorrectable ECC error was detected during execution."), - 215: ("This indicates that the ::cudaLimit passed to the API call is not supported by the active device."), - 216: ( - "This indicates that a call tried to access an exclusive-thread device that" - " is already in use by a different thread." - ), - 217: ("This error indicates that P2P access is not supported across the given devices."), - 218: ( - "A PTX compilation failed. The runtime may fall back to compiling PTX if" - " an application does not contain a suitable binary for the current device." - ), - 219: "This indicates an error with the OpenGL or DirectX context.", - 220: ("This indicates that an uncorrectable NVLink error was detected during the execution."), - 221: ( - "This indicates that the PTX JIT compiler library was not found. The JIT Compiler" - " library is used for PTX compilation. The runtime may fall back to compiling PTX" - " if an application does not contain a suitable binary for the current device." - ), - 222: ( - "This indicates that the provided PTX was compiled with an unsupported toolchain." - " The most common reason for this, is the PTX was generated by a compiler newer" - " than what is supported by the CUDA driver and PTX JIT compiler." - ), - 223: ( - "This indicates that the JIT compilation was disabled. The JIT compilation compiles" - " PTX. The runtime may fall back to compiling PTX if an application does not contain" - " a suitable binary for the current device." - ), - 224: "This indicates that the provided execution affinity is not supported by the device.", - 225: ( - "This indicates that the code to be compiled by the PTX JIT contains unsupported call to cudaDeviceSynchronize." - ), - 226: ( - "This indicates that an exception occurred on the device that is now" - " contained by the GPU's error containment capability. Common causes are -" - " a. Certain types of invalid accesses of peer GPU memory over nvlink" - " b. Certain classes of hardware errors" - " This leaves the process in an inconsistent state and any further CUDA" - " work will return the same error. To continue using CUDA, the process must" - " be terminated and relaunched." - ), - 300: "This indicates that the device kernel source is invalid.", - 301: "This indicates that the file specified was not found.", - 302: "This indicates that a link to a shared object failed to resolve.", - 303: "This indicates that initialization of a shared object failed.", - 304: "This error indicates that an OS call failed.", - 400: ( - "This indicates that a resource handle passed to the API call was not" - " valid. Resource handles are opaque types like ::cudaStream_t and" - " ::cudaEvent_t." - ), - 401: ( - "This indicates that a resource required by the API call is not in a" - " valid state to perform the requested operation." - ), - 402: ( - "This indicates an attempt was made to introspect an object in a way that" - " would discard semantically important information. This is either due to" - " the object using funtionality newer than the API version used to" - " introspect it or omission of optional return arguments." - ), - 500: ( - "This indicates that a named symbol was not found. Examples of symbols" - " are global/constant variable names, driver function names, texture names," - " and surface names." - ), - 600: ( - "This indicates that asynchronous operations issued previously have not" - " completed yet. This result is not actually an error, but must be indicated" - " differently than ::cudaSuccess (which indicates completion). Calls that" - " may return this value include ::cudaEventQuery() and ::cudaStreamQuery()." - ), - 700: ( - "The device encountered a load or store instruction on an invalid memory address." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 701: ( - "This indicates that a launch did not occur because it did not have" - " appropriate resources. Although this error is similar to" - " ::cudaErrorInvalidConfiguration, this error usually indicates that the" - " user has attempted to pass too many arguments to the device kernel, or the" - " kernel launch specifies too many threads for the kernel's register count." - ), - 702: ( - "This indicates that the device kernel took too long to execute. This can" - " only occur if timeouts are enabled - see the device attribute" - ' ::cudaDeviceAttr::cudaDevAttrKernelExecTimeout "cudaDevAttrKernelExecTimeout"' - " for more information." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 703: ("This error indicates a kernel launch that uses an incompatible texturing mode."), - 704: ( - "This error indicates that a call to ::cudaDeviceEnablePeerAccess() is" - " trying to re-enable peer addressing on from a context which has already" - " had peer addressing enabled." - ), - 705: ( - "This error indicates that ::cudaDeviceDisablePeerAccess() is trying to" - " disable peer addressing which has not been enabled yet via" - " ::cudaDeviceEnablePeerAccess()." - ), - 708: ( - "This indicates that the user has called ::cudaSetValidDevices()," - " ::cudaSetDeviceFlags(), ::cudaD3D9SetDirect3DDevice()," - " ::cudaD3D10SetDirect3DDevice, ::cudaD3D11SetDirect3DDevice(), or" - " ::cudaVDPAUSetVDPAUDevice() after initializing the CUDA runtime by" - " calling non-device management operations (allocating memory and" - " launching kernels are examples of non-device management operations)." - " This error can also be returned if using runtime/driver" - " interoperability and there is an existing ::CUcontext active on the" - " host thread." - ), - 709: ( - "This error indicates that the context current to the calling thread" - " has been destroyed using ::cuCtxDestroy, or is a primary context which" - " has not yet been initialized." - ), - 710: ( - "An assert triggered in device code during kernel execution. The device" - " cannot be used again. All existing allocations are invalid. To continue" - " using CUDA, the process must be terminated and relaunched." - ), - 711: ( - "This error indicates that the hardware resources required to enable" - " peer access have been exhausted for one or more of the devices" - " passed to ::cudaEnablePeerAccess()." - ), - 712: ("This error indicates that the memory range passed to ::cudaHostRegister() has already been registered."), - 713: ( - "This error indicates that the pointer passed to ::cudaHostUnregister()" - " does not correspond to any currently registered memory region." - ), - 714: ( - "Device encountered an error in the call stack during kernel execution," - " possibly due to stack corruption or exceeding the stack size limit." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 715: ( - "The device encountered an illegal instruction during kernel execution" - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 716: ( - "The device encountered a load or store instruction" - " on a memory address which is not aligned." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 717: ( - "While executing a kernel, the device encountered an instruction" - " which can only operate on memory locations in certain address spaces" - " (global, shared, or local), but was supplied a memory address not" - " belonging to an allowed address space." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 718: ( - "The device encountered an invalid program counter." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 719: ( - "An exception occurred on the device while executing a kernel. Common" - " causes include dereferencing an invalid device pointer and accessing" - " out of bounds shared memory. Less common cases can be system specific - more" - " information about these cases can be found in the system specific user guide." - " This leaves the process in an inconsistent state and any further CUDA work" - " will return the same error. To continue using CUDA, the process must be terminated" - " and relaunched." - ), - 720: ( - "This error indicates that the number of blocks launched per grid for a kernel that was" - " launched via either ::cudaLaunchCooperativeKernel" - " exceeds the maximum number of blocks as allowed by ::cudaOccupancyMaxActiveBlocksPerMultiprocessor" - " or ::cudaOccupancyMaxActiveBlocksPerMultiprocessorWithFlags times the number of multiprocessors" - " as specified by the device attribute ::cudaDevAttrMultiProcessorCount." - ), - 721: ( - "An exception occurred on the device while exiting a kernel using tensor memory: the" - " tensor memory was not completely deallocated. This leaves the process in an inconsistent" - " state and any further CUDA work will return the same error. To continue using CUDA, the" - " process must be terminated and relaunched." - ), - 800: "This error indicates the attempted operation is not permitted.", - 801: ("This error indicates the attempted operation is not supported on the current system or device."), - 802: ( - "This error indicates that the system is not yet ready to start any CUDA" - " work. To continue using CUDA, verify the system configuration is in a" - " valid state and all required driver daemons are actively running." - " More information about this error can be found in the system specific" - " user guide." - ), - 803: ( - "This error indicates that there is a mismatch between the versions of" - " the display driver and the CUDA driver. Refer to the compatibility documentation" - " for supported versions." - ), - 804: ( - "This error indicates that the system was upgraded to run with forward compatibility" - " but the visible hardware detected by CUDA does not support this configuration." - " Refer to the compatibility documentation for the supported hardware matrix or ensure" - " that only supported hardware is visible during initialization via the CUDA_VISIBLE_DEVICES" - " environment variable." - ), - 805: "This error indicates that the MPS client failed to connect to the MPS control daemon or the MPS server.", - 806: "This error indicates that the remote procedural call between the MPS server and the MPS client failed.", - 807: ( - "This error indicates that the MPS server is not ready to accept new MPS client requests." - " This error can be returned when the MPS server is in the process of recovering from a fatal failure." - ), - 808: "This error indicates that the hardware resources required to create MPS client have been exhausted.", - 809: "This error indicates the the hardware resources required to device connections have been exhausted.", - 810: "This error indicates that the MPS client has been terminated by the server. To continue using CUDA, the process must be terminated and relaunched.", - 811: "This error indicates, that the program is using CUDA Dynamic Parallelism, but the current configuration, like MPS, does not support it.", - 812: "This error indicates, that the program contains an unsupported interaction between different versions of CUDA Dynamic Parallelism.", - 900: "The operation is not permitted when the stream is capturing.", - 901: ("The current capture sequence on the stream has been invalidated due to a previous error."), - 902: ("The operation would have resulted in a merge of two independent capture sequences."), - 903: "The capture was not initiated in this stream.", - 904: ("The capture sequence contains a fork that was not joined to the primary stream."), - 905: ( - "A dependency would have been created which crosses the capture sequence" - " boundary. Only implicit in-stream ordering dependencies are allowed to" - " cross the boundary." - ), - 906: ( - "The operation would have resulted in a disallowed implicit dependency on" - " a current capture sequence from cudaStreamLegacy." - ), - 907: ("The operation is not permitted on an event which was last recorded in a capturing stream."), - 908: ( - "A stream capture sequence not initiated with the ::cudaStreamCaptureModeRelaxed" - " argument to ::cudaStreamBeginCapture was passed to ::cudaStreamEndCapture in a" - " different thread." - ), - 909: "This indicates that the wait operation has timed out.", - 910: ( - "This error indicates that the graph update was not performed because it included" - " changes which violated constraints specific to instantiated graph update." - ), - 911: ( - "This indicates that an async error has occurred in a device outside of CUDA." - " If CUDA was waiting for an external device's signal before consuming shared data," - " the external device signaled an error indicating that the data is not valid for" - " consumption. This leaves the process in an inconsistent state and any further CUDA" - " work will return the same error. To continue using CUDA, the process must be" - " terminated and relaunched." - ), - 912: ("This indicates that a kernel launch error has occurred due to cluster misconfiguration."), - 913: ("Indiciates a function handle is not loaded when calling an API that requires a loaded function."), - 914: ("This error indicates one or more resources passed in are not valid resource types for the operation."), - 915: ("This error indicates one or more resources are insufficient or non-applicable for the operation."), - 917: ( - "This error indicates that the requested operation is not permitted because the" - " stream is in a detached state. This can occur if the green context associated" - " with the stream has been destroyed, limiting the stream's operational capabilities." - ), - 999: "This indicates that an unknown internal error has occurred.", - 10000: ( - "Any unhandled CUDA driver error is added to this value and returned via" - " the runtime. Production releases of CUDA should not return such errors." - " This error return is deprecated as of CUDA 4.1." - ), -} diff --git a/cuda_core/cuda/core/_utils/version.pxd b/cuda_core/cuda/core/_utils/version.pxd index 2746d463dba..075ce846add 100644 --- a/cuda_core/cuda/core/_utils/version.pxd +++ b/cuda_core/cuda/core/_utils/version.pxd @@ -2,5 +2,4 @@ # # SPDX-License-Identifier: Apache-2.0 -cdef tuple cy_binding_version() cdef tuple cy_driver_version() diff --git a/cuda_core/cuda/core/_utils/version.pyi b/cuda_core/cuda/core/_utils/version.pyi index db2e27a57d0..128b2ff4f20 100644 --- a/cuda_core/cuda/core/_utils/version.pyi +++ b/cuda_core/cuda/core/_utils/version.pyi @@ -2,6 +2,7 @@ import functools +BUILD_CUDA_MAJOR: int = ... def _parse_version_triple(version_str: str) -> tuple[int, int, int]: """Parse a PEP 440 version string into a (major, minor, patch) triple. diff --git a/cuda_core/cuda/core/_utils/version.pyx b/cuda_core/cuda/core/_utils/version.pyx index ed4c93c0262..ae396af940d 100644 --- a/cuda_core/cuda/core/_utils/version.pyx +++ b/cuda_core/cuda/core/_utils/version.pyx @@ -8,6 +8,13 @@ import re from cuda.core._utils.cuda_utils import driver, handle_return +# The CUDA major series this build of cuda.core targets (12 or 13), from the +# compile-time environment build_hooks.py sets. The installed cuda-bindings has +# the same major (cuda/core/__init__.py enforces it at import). Python modules +# that must branch on the series, where `IF CUDA_CORE_BUILD_MAJOR` is not +# available, read this instead of comparing binding_version(). +BUILD_CUDA_MAJOR: int = CUDA_CORE_BUILD_MAJOR + def _parse_version_triple(version_str: str) -> tuple[int, int, int]: """Parse a PEP 440 version string into a (major, minor, patch) triple. @@ -38,17 +45,9 @@ def driver_version() -> tuple[int, int, int]: return (ver // 1000, (ver // 10) % 100, ver % 10) -cdef tuple _cached_binding_version = None cdef tuple _cached_driver_version = None -cdef tuple cy_binding_version(): - global _cached_binding_version - if _cached_binding_version is None: - _cached_binding_version = binding_version() - return _cached_binding_version - - cdef tuple cy_driver_version(): global _cached_driver_version if _cached_driver_version is None: diff --git a/cuda_core/cuda/core/checkpoint.py b/cuda_core/cuda/core/checkpoint.py index 32fe7e26d83..cf9e09ed14c 100644 --- a/cuda_core/cuda/core/checkpoint.py +++ b/cuda_core/cuda/core/checkpoint.py @@ -8,7 +8,7 @@ from cuda.bindings import driver as _driver from cuda.core._utils.cuda_utils import handle_return as _handle_cuda_return -from cuda.core._utils.version import binding_version as _binding_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR as _BUILD_CUDA_MAJOR from cuda.core._utils.version import driver_version as _driver_version from cuda.core.typing import ProcessStateType as _ProcessStateType @@ -19,18 +19,6 @@ ("CU_PROCESS_STATE_FAILED", "failed"), ) -_REQUIRED_BINDING_ATTRS = ( - "cuCheckpointProcessCheckpoint", - "cuCheckpointProcessGetRestoreThreadId", - "cuCheckpointProcessGetState", - "cuCheckpointProcessLock", - "cuCheckpointProcessRestore", - "cuCheckpointProcessUnlock", - "CUcheckpointGpuPair", - "CUcheckpointLockArgs", - "CUprocessState", - "CUcheckpointRestoreArgs", -) _REQUIRED_DRIVER_VERSION = (12, 8, 0) _driver_capability_checked = False @@ -130,18 +118,10 @@ def _get_driver() -> Any: if _driver_capability_checked: return _driver - binding_ver = _binding_version() - if not _binding_version_supports_checkpoint(binding_ver): - raise RuntimeError( - "CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. " - f"Found cuda.bindings {'.'.join(str(part) for part in binding_ver[:3])}." - ) - - missing = [name for name in _REQUIRED_BINDING_ATTRS if not hasattr(_driver, name)] - if missing: - raise RuntimeError( - f"CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. Missing: {', '.join(missing)}" - ) + # Restoring onto other GPUs uses CUcheckpointGpuPair, a CUDA 13 type that + # the CUDA 12 build's cuda-bindings does not have. + if _BUILD_CUDA_MAJOR < 13: + raise RuntimeError("CUDA checkpointing requires the CUDA 13 build of cuda.core (cuda-core[cu13]).") driver_ver = _driver_version() if driver_ver < _REQUIRED_DRIVER_VERSION: @@ -154,11 +134,6 @@ def _get_driver() -> Any: return _driver -def _binding_version_supports_checkpoint(version: tuple[int, ...]) -> bool: - major, minor, patch = version[:3] - return (major == 12 and (minor, patch) >= (8, 0)) or (major == 13 and (minor, patch) >= (0, 2)) or major > 13 - - def _get_process_state_names(driver: Any) -> dict[Any, _ProcessStateType]: return {getattr(driver.CUprocessState, attr): state_name for attr, state_name in _PROCESS_STATE_NAME_ATTRS} diff --git a/cuda_core/cuda/core/graph/_graph_builder.pyx b/cuda_core/cuda/core/graph/_graph_builder.pyx index 03672316bef..8dd3ac6d3a4 100644 --- a/cuda_core/cuda/core/graph/_graph_builder.pyx +++ b/cuda_core/cuda/core/graph/_graph_builder.pyx @@ -52,7 +52,7 @@ from cuda.core._rt cimport ( ) from cuda.core._stream cimport Stream, Stream_accept from cuda.core._utils.cuda_utils cimport HANDLE_RETURN -from cuda.core._utils.version cimport cy_binding_version, cy_driver_version +from cuda.core._utils.version cimport cy_driver_version from cuda.core._utils.cuda_utils import ( CUDAError, @@ -251,10 +251,7 @@ def _instantiate_graph(source, options: GraphCompleteOptions | None = None) -> G ) elif params.result_out == driver.CUgraphInstantiateResult.CUDA_GRAPH_INSTANTIATE_MULTIPLE_CTXS_NOT_SUPPORTED: raise RuntimeError("Instantiation for device launch failed due to the nodes belonging to different contexts.") - elif ( - cy_binding_version() >= (12, 8, 0) - and params.result_out == driver.CUgraphInstantiateResult.CUDA_GRAPH_INSTANTIATE_CONDITIONAL_HANDLE_UNUSED - ): + elif params.result_out == driver.CUgraphInstantiateResult.CUDA_GRAPH_INSTANTIATE_CONDITIONAL_HANDLE_UNUSED: raise RuntimeError("One or more conditional handles are not associated with conditional builders.") elif params.result_out != driver.CUgraphInstantiateResult.CUDA_GRAPH_INSTANTIATE_SUCCESS: raise RuntimeError(f"Graph instantiation failed with unexpected error code: {params.result_out}") @@ -666,8 +663,6 @@ cdef class GraphBuilder: GB_check_open(self) if cy_driver_version() < (12, 3, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional handles") - if cy_binding_version() < (12, 3, 0): - raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional handles") if default_value is not None: flags = driver.CU_GRAPH_COND_ASSIGN_DEFAULT else: @@ -706,8 +701,6 @@ cdef class GraphBuilder: GB_check_open(self) if cy_driver_version() < (12, 3, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional if") - if cy_binding_version() < (12, 3, 0): - raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional if") if not isinstance(condition, GraphCondition): raise TypeError( f"condition must be a GraphCondition object (from " @@ -743,8 +736,6 @@ cdef class GraphBuilder: GB_check_open(self) if cy_driver_version() < (12, 8, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional if-else") - if cy_binding_version() < (12, 8, 0): - raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional if-else") if not isinstance(condition, GraphCondition): raise TypeError( f"condition must be a GraphCondition object (from " @@ -783,8 +774,6 @@ cdef class GraphBuilder: GB_check_open(self) if cy_driver_version() < (12, 8, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional switch") - if cy_binding_version() < (12, 8, 0): - raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional switch") if not isinstance(condition, GraphCondition): raise TypeError( f"condition must be a GraphCondition object (from " @@ -820,8 +809,6 @@ cdef class GraphBuilder: GB_check_open(self) if cy_driver_version() < (12, 3, 0): raise RuntimeError(f"Driver version {'.'.join(map(str, cy_driver_version()))} does not support conditional while loop") - if cy_binding_version() < (12, 3, 0): - raise RuntimeError(f"Binding version {'.'.join(map(str, cy_binding_version()))} does not support conditional while loop") if not isinstance(condition, GraphCondition): raise TypeError( f"condition must be a GraphCondition object (from " diff --git a/cuda_core/cuda/core/graph/_subclasses.pxd b/cuda_core/cuda/core/graph/_subclasses.pxd index c85f5e3f201..b7c37c64bcb 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pxd +++ b/cuda_core/cuda/core/graph/_subclasses.pxd @@ -162,6 +162,9 @@ cdef class ConditionalNode(GraphNode): @staticmethod cdef ConditionalNode _create_from_driver(GraphNodeHandle h_node) + IF CUDA_CORE_BUILD_MAJOR >= 13: + @staticmethod + cdef ConditionalNode _create_from_driver_params(GraphNodeHandle h_node) cdef class IfNode(ConditionalNode): diff --git a/cuda_core/cuda/core/graph/_subclasses.pyx b/cuda_core/cuda/core/graph/_subclasses.pyx index 0f77cabfb39..1e1d2c1953c 100644 --- a/cuda_core/cuda/core/graph/_subclasses.pyx +++ b/cuda_core/cuda/core/graph/_subclasses.pyx @@ -60,14 +60,13 @@ from cuda.core._rt cimport ( make_opaque_py, ) from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value -from cuda.core._utils.version cimport cy_binding_version, cy_driver_version +from cuda.core._utils.version cimport cy_driver_version from cuda.core.graph._host_callback cimport ( _is_py_host_trampoline, _resolve_host_callback, ) -from cuda.core._utils.cuda_utils import driver, handle_return from cuda.core.typing import GraphConditionalType __all__ = [ @@ -97,10 +96,6 @@ __all__ = [ ] -cdef bint _has_cuGraphNodeGetParams = False -cdef bint _version_checked = False - - cdef void _require_graph_node_update_support() except *: cdef tuple version = cy_driver_version() if version < (12, 2, 0): @@ -108,12 +103,6 @@ cdef void _require_graph_node_update_support() except *: "Graph node mutation requires CUDA driver 12.2 or newer; " f"using driver version {'.'.join(map(str, version))}" ) - version = cy_binding_version() - if version < (12, 2, 0): - raise RuntimeError( - "Graph node mutation requires cuda.bindings 12.2 or newer; " - f"using cuda.bindings version {'.'.join(map(str, version))}" - ) cdef void _set_definition_node_params( @@ -214,14 +203,22 @@ cdef void _set_executable_node_enabled( cdef bint _check_node_get_params(): - global _has_cuGraphNodeGetParams, _version_checked - if not _version_checked: - from cuda.core._utils.version import binding_version, driver_version - _has_cuGraphNodeGetParams = ( - driver_version() >= (13, 2, 0) and binding_version() >= (13, 2, 0) - ) - _version_checked = True - return _has_cuGraphNodeGetParams + """Whether cuGraphNodeGetParams (CUDA 13.2) can be called. + + The CUDA 13 build always has the binding; only the driver can lack it.""" + IF CUDA_CORE_BUILD_MAJOR >= 13: + return cy_driver_version() >= (13, 2, 0) + ELSE: + return False + + +IF CUDA_CORE_BUILD_MAJOR >= 13: + cdef void _node_get_params( + cydriver.CUgraphNode node, + cydriver.CUgraphNodeParams* params) except *: + c_memset(params, 0, sizeof(params[0])) + with nogil: + HANDLE_RETURN(cydriver.cuGraphNodeGetParams(node, params)) cdef void _reject_unsupported_kernel_node( @@ -671,7 +668,7 @@ cdef class MemsetNode(GraphNode): cdef cydriver.CUcontext ctx = NULL cdef cydriver.CUDA_MEMSET_NODE_PARAMS current cdef cydriver.CUgraphNodeParams params - cdef object queried + cdef cydriver.CUgraphNodeParams queried # no-cython-lint if dst is None and dst_owner is not None: raise ValueError("dst_owner requires dst") @@ -684,11 +681,14 @@ cdef class MemsetNode(GraphNode): with nogil: HANDLE_RETURN(cydriver.cuGraphMemsetNodeGetParams( node, ¤t)) - if _check_node_get_params(): - queried = handle_return(driver.cuGraphNodeGetParams( - node)) - ctx = int(queried.memset.ctx) - else: + IF CUDA_CORE_BUILD_MAJOR >= 13: + if _check_node_get_params(): + _node_get_params(node, &queried) + ctx = queried.memset.ctx + else: + with nogil: + HANDLE_RETURN(cydriver.cuCtxGetCurrent(&ctx)) + ELSE: with nogil: HANDLE_RETURN(cydriver.cuCtxGetCurrent(&ctx)) @@ -861,7 +861,7 @@ cdef class MemcpyNode(GraphNode): cdef cydriver.CUgraphNodeParams params cdef cydriver.CUmemorytype c_dst_type cdef cydriver.CUmemorytype c_src_type - cdef object queried + cdef cydriver.CUgraphNodeParams queried # no-cython-lint if dst is None and dst_owner is not None: raise ValueError("dst_owner requires dst") @@ -875,12 +875,14 @@ cdef class MemcpyNode(GraphNode): with nogil: HANDLE_RETURN(cydriver.cuGraphMemcpyNodeGetParams( node, ¶ms.memcpy.copyParams)) - if _check_node_get_params(): - queried = handle_return(driver.cuGraphNodeGetParams( - node)) - ctx = int( - queried.memcpy.copyCtx) - else: + IF CUDA_CORE_BUILD_MAJOR >= 13: + if _check_node_get_params(): + _node_get_params(node, &queried) + ctx = queried.memcpy.copyCtx + else: + with nogil: + HANDLE_RETURN(cydriver.cuCtxGetCurrent(&ctx)) + ELSE: with nogil: HANDLE_RETURN(cydriver.cuCtxGetCurrent(&ctx)) params.memcpy.copyCtx = ctx @@ -1262,47 +1264,52 @@ cdef class ConditionalNode(GraphNode): n._cond_type = cydriver.CU_GRAPH_COND_TYPE_IF n._branches = () return n - - cdef cydriver.CUgraphNode node = as_cu(h_node) - params = handle_return(driver.cuGraphNodeGetParams( - node)) - cond_params = params.conditional - cdef int cond_type_int = int(cond_params.type) - cdef unsigned int size = int(cond_params.size) - - cdef GraphCondition condition = GraphCondition.__new__(GraphCondition) - condition._c_handle = ( - int(cond_params.handle)) - - cdef GraphHandle h_graph = graph_node_get_graph(h_node) - cdef list branch_list = [] - cdef unsigned int i - cdef GraphHandle h_branch - if cond_params.phGraph_out is not None: - for i in range(size): - h_branch = create_child_graph_handle( - int(cond_params.phGraph_out[i]), - h_graph, node) - branch_list.append(GraphDefinition._from_handle(h_branch)) - cdef tuple branches = tuple(branch_list) - - cdef type cls - if cond_type_int == cydriver.CU_GRAPH_COND_TYPE_IF: - if size == 1: - cls = IfNode + IF CUDA_CORE_BUILD_MAJOR >= 13: + return ConditionalNode._create_from_driver_params(h_node) + ELSE: + raise AssertionError("unreachable: cuGraphNodeGetParams needs the CUDA 13 build") + + IF CUDA_CORE_BUILD_MAJOR >= 13: + @staticmethod + cdef ConditionalNode _create_from_driver_params(GraphNodeHandle h_node): + cdef ConditionalNode n + cdef cydriver.CUgraphNode node = as_cu(h_node) + cdef cydriver.CUgraphNodeParams params + _node_get_params(node, ¶ms) + cdef int cond_type_int = params.conditional.type + cdef unsigned int size = params.conditional.size + + cdef GraphCondition condition = GraphCondition.__new__(GraphCondition) + condition._c_handle = params.conditional.handle + + cdef GraphHandle h_graph = graph_node_get_graph(h_node) + cdef list branch_list = [] + cdef unsigned int i + cdef GraphHandle h_branch + if params.conditional.phGraph_out != NULL: + for i in range(size): + h_branch = create_child_graph_handle( + params.conditional.phGraph_out[i], h_graph, node) + branch_list.append(GraphDefinition._from_handle(h_branch)) + cdef tuple branches = tuple(branch_list) + + cdef type cls + if cond_type_int == cydriver.CU_GRAPH_COND_TYPE_IF: + if size == 1: + cls = IfNode + else: + cls = IfElseNode + elif cond_type_int == cydriver.CU_GRAPH_COND_TYPE_WHILE: + cls = WhileNode else: - cls = IfElseNode - elif cond_type_int == cydriver.CU_GRAPH_COND_TYPE_WHILE: - cls = WhileNode - else: - cls = SwitchNode + cls = SwitchNode - n = cls.__new__(cls) - n._h_node = h_node - n._condition = condition - n._cond_type = cond_type_int - n._branches = branches - return n + n = cls.__new__(cls) + n._h_node = h_node + n._condition = condition + n._cond_type = cond_type_int + n._branches = branches + return n def __repr__(self) -> str: return f"" diff --git a/cuda_core/cuda/core/system/__init__.py b/cuda_core/cuda/core/system/__init__.py index acb648549bc..b73927f0f6f 100644 --- a/cuda_core/cuda/core/system/__init__.py +++ b/cuda_core/cuda/core/system/__init__.py @@ -8,8 +8,6 @@ # contexts created, so that a user can use NVML to explore things about their # system without loading CUDA. -from typing import TYPE_CHECKING - __all__ = [ "CUDA_BINDINGS_NVML_IS_COMPATIBLE", "get_driver_branch", @@ -23,25 +21,14 @@ from cuda.core.system import typing +from ._device import * +from ._device import __all__ as _device_all from ._system import * - -# The TYPE_CHECKING branch is split out from the runtime branch so that -# stubgen-pyx, which only recognizes the literal `if TYPE_CHECKING:` form, -# preserves these imports in the generated .pyi. When -# CUDA_BINDINGS_NVML_IS_COMPATIBLE is no longer necessary, this complexity can -# be removed. -if TYPE_CHECKING: - from ._device import * - from ._system_events import * - from .exceptions import * -elif CUDA_BINDINGS_NVML_IS_COMPATIBLE: - from ._device import * - from ._device import __all__ as _device_all - from ._system_events import * - from ._system_events import __all__ as _system_events_all - from .exceptions import * - from .exceptions import __all__ as _exceptions_all - - __all__.extend(_device_all) - __all__.extend(_system_events_all) - __all__.extend(_exceptions_all) +from ._system_events import * +from ._system_events import __all__ as _system_events_all +from .exceptions import * +from .exceptions import __all__ as _exceptions_all + +__all__.extend(_device_all) +__all__.extend(_system_events_all) +__all__.extend(_exceptions_all) diff --git a/cuda_core/cuda/core/system/_device.pyi b/cuda_core/cuda/core/system/_device.pyi index 3e2a6bd018c..6fea049b21b 100644 --- a/cuda_core/cuda/core/system/_device.pyi +++ b/cuda_core/cuda/core/system/_device.pyi @@ -22,8 +22,7 @@ _EVENT_TYPE_MAPPING = {nvml.EventType.NONE: EventType.NONE, nvml.EventType.SINGL _EVENT_TYPE_INV_MAPPING = {v: k for k, v in _EVENT_TYPE_MAPPING.items()} _FAN_CONTROL_POLICY_MAPPING = {nvml.FanControlPolicy.TEMPERATURE_CONTINUOUS_SW: FanControlPolicy.TEMPERATURE_CONTROLLED, nvml.FanControlPolicy.MANUAL: FanControlPolicy.MANUAL} _INFOROM_OBJECT_MAPPING = {InforomObject.OEM: nvml.InforomObject.INFOROM_OEM, InforomObject.ECC: nvml.InforomObject.INFOROM_ECC, InforomObject.POWER: nvml.InforomObject.INFOROM_POWER, InforomObject.DEN: nvml.InforomObject.INFOROM_DEN} -_NVLINK_VERSION_MAPPING = {nvml.NvlinkVersion.VERSION_1_0: (1, 0), nvml.NvlinkVersion.VERSION_2_0: (2, 0), nvml.NvlinkVersion.VERSION_2_2: (2, 2), nvml.NvlinkVersion.VERSION_3_0: (3, 0), nvml.NvlinkVersion.VERSION_3_1: (3, 1), nvml.NvlinkVersion.VERSION_4_0: (4, 0), nvml.NvlinkVersion.VERSION_5_0: (5, 0)} -_NVLINK_VERSION_6_0 = getattr(nvml.NvlinkVersion, 'VERSION_6_0', None) +_NVLINK_VERSION_MAPPING = {nvml.NvlinkVersion.VERSION_1_0: (1, 0), nvml.NvlinkVersion.VERSION_2_0: (2, 0), nvml.NvlinkVersion.VERSION_2_2: (2, 2), nvml.NvlinkVersion.VERSION_3_0: (3, 0), nvml.NvlinkVersion.VERSION_3_1: (3, 1), nvml.NvlinkVersion.VERSION_4_0: (4, 0), nvml.NvlinkVersion.VERSION_5_0: (5, 0), nvml.NvlinkVersion.VERSION_6_0: (6, 0)} _TEMPERATURE_THRESHOLD_MAPPING = {TemperatureThresholds.SHUTDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SHUTDOWN, TemperatureThresholds.SLOWDOWN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_SLOWDOWN, TemperatureThresholds.MEM_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_MEM_MAX, TemperatureThresholds.GPU_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_GPU_MAX, TemperatureThresholds.ACOUSTIC_MIN: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_MIN, TemperatureThresholds.ACOUSTIC_CURR: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_CURR, TemperatureThresholds.ACOUSTIC_MAX: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_ACOUSTIC_MAX, TemperatureThresholds.GPS_CURR: nvml.TemperatureThresholds.TEMPERATURE_THRESHOLD_GPS_CURR} _THERMAL_CONTROLLER_MAPPING = {nvml.ThermalController.GPU_INTERNAL: ThermalController.GPU_INTERNAL, nvml.ThermalController.ADM1032: ThermalController.ADM1032, nvml.ThermalController.ADT7461: ThermalController.ADT7461, nvml.ThermalController.MAX6649: ThermalController.MAX6649, nvml.ThermalController.MAX1617: ThermalController.MAX1617, nvml.ThermalController.LM99: ThermalController.LM99, nvml.ThermalController.LM89: ThermalController.LM89, nvml.ThermalController.LM64: ThermalController.LM64, nvml.ThermalController.G781: ThermalController.G781, nvml.ThermalController.ADT7473: ThermalController.ADT7473, nvml.ThermalController.SBMAX6649: ThermalController.SBMAX6649, nvml.ThermalController.VBIOSEVT: ThermalController.VBIOSEVT, nvml.ThermalController.OS: ThermalController.OS, nvml.ThermalController.NVSYSCON_CANOAS: ThermalController.NVSYSCON_CANOAS, nvml.ThermalController.NVSYSCON_E551: ThermalController.NVSYSCON_E551, nvml.ThermalController.MAX6649R: ThermalController.MAX6649R, nvml.ThermalController.ADT7473S: ThermalController.ADT7473S, nvml.ThermalController.UNKNOWN: ThermalController.UNKNOWN} _THERMAL_TARGET_MAPPING = {nvml.ThermalTarget.NONE: ThermalTarget.NONE, nvml.ThermalTarget.GPU: ThermalTarget.GPU, nvml.ThermalTarget.MEMORY: ThermalTarget.MEMORY, nvml.ThermalTarget.POWER_SUPPLY: ThermalTarget.POWER_SUPPLY, nvml.ThermalTarget.BOARD: ThermalTarget.BOARD, nvml.ThermalTarget.VCD_BOARD: ThermalTarget.VCD_BOARD, nvml.ThermalTarget.VCD_INLET: ThermalTarget.VCD_INLET, nvml.ThermalTarget.VCD_OUTLET: ThermalTarget.VCD_OUTLET, nvml.ThermalTarget.ALL: ThermalTarget.ALL} diff --git a/cuda_core/cuda/core/system/_nvlink.pxi b/cuda_core/cuda/core/system/_nvlink.pxi index 49ac1b75ba1..acd64c63fed 100644 --- a/cuda_core/cuda/core/system/_nvlink.pxi +++ b/cuda_core/cuda/core/system/_nvlink.pxi @@ -11,12 +11,9 @@ _NVLINK_VERSION_MAPPING = { nvml.NvlinkVersion.VERSION_3_1: (3, 1), nvml.NvlinkVersion.VERSION_4_0: (4, 0), nvml.NvlinkVersion.VERSION_5_0: (5, 0), + nvml.NvlinkVersion.VERSION_6_0: (6, 0), } -_NVLINK_VERSION_6_0 = getattr(nvml.NvlinkVersion, "VERSION_6_0", None) -if _NVLINK_VERSION_6_0 is not None: - _NVLINK_VERSION_MAPPING[_NVLINK_VERSION_6_0] = (6, 0) - class _NvlinkInfoMeta(type): @property diff --git a/cuda_core/cuda/core/system/_system.pyi b/cuda_core/cuda/core/system/_system.pyi index b29b16f16aa..9306795f3c4 100644 --- a/cuda_core/cuda/core/system/_system.pyi +++ b/cuda_core/cuda/core/system/_system.pyi @@ -1,6 +1,6 @@ # This file was generated by stubgen-pyx v0.2.22 from cuda_core/cuda/core/system/_system.pyx -CUDA_BINDINGS_NVML_IS_COMPATIBLE: bool +CUDA_BINDINGS_NVML_IS_COMPATIBLE: bool = True __all__ = ['get_driver_branch', 'get_kernel_mode_driver_version', 'get_user_mode_driver_version', 'get_nvml_version', 'get_num_devices', 'get_process_name', 'CUDA_BINDINGS_NVML_IS_COMPATIBLE'] def get_user_mode_driver_version() -> tuple[int, ...]: @@ -8,7 +8,7 @@ def get_user_mode_driver_version() -> tuple[int, ...]: Get the user-mode (UMD / CUDA) driver version. This is the most commonly needed version when checking CUDA driver - compatibility. It works with all ``cuda-bindings`` versions. + compatibility. Returns ------- diff --git a/cuda_core/cuda/core/system/_system.pyx b/cuda_core/cuda/core/system/_system.pyx index 2a6c8ffc23d..c7414f46e1a 100644 --- a/cuda_core/cuda/core/system/_system.pyx +++ b/cuda_core/cuda/core/system/_system.pyx @@ -3,12 +3,15 @@ # SPDX-License-Identifier: Apache-2.0 -# This file needs to either use NVML exclusively, or when `cuda.bindings.nvml` -# isn't available, fall back to non-NVML-based methods for backward -# compatibility. +# cuda.core.system uses NVML through cuda.bindings.nvml, which every +# cuda-bindings cuda.core accepts provides (cuda/core/_bindings_floor.py). +# Loading the NVML library itself happens in initialize(), on first use, so +# this module stays importable without CUDA or NVML installed. -CUDA_BINDINGS_NVML_IS_COMPATIBLE: bool +# Always True: kept for callers that read it before the cuda-bindings floor +# made NVML support unconditional. Deprecated. +CUDA_BINDINGS_NVML_IS_COMPATIBLE: bool = True # Please keep in sync with the equivalent implementation in @@ -36,23 +39,8 @@ else: c_locale_guard = None -try: - from cuda.bindings._version import __version_tuple__ as _BINDINGS_VERSION -except ImportError: - CUDA_BINDINGS_NVML_IS_COMPATIBLE = False -else: - CUDA_BINDINGS_NVML_IS_COMPATIBLE = _BINDINGS_VERSION >= (13, 2, 0) or (_BINDINGS_VERSION[0] == 12 and _BINDINGS_VERSION[1:3] >= (9, 6)) - - -if CUDA_BINDINGS_NVML_IS_COMPATIBLE: - try: - from cuda.bindings import nvml - except ImportError: - CUDA_BINDINGS_NVML_IS_COMPATIBLE = False - - from cuda.core.system._nvml_context import initialize -else: - from cuda.core._utils.cuda_utils import driver, handle_return, runtime +from cuda.bindings import nvml +from cuda.core.system._nvml_context import initialize def get_user_mode_driver_version() -> tuple[int, ...]: @@ -60,7 +48,7 @@ def get_user_mode_driver_version() -> tuple[int, ...]: Get the user-mode (UMD / CUDA) driver version. This is the most commonly needed version when checking CUDA driver - compatibility. It works with all ``cuda-bindings`` versions. + compatibility. Returns ------- @@ -68,11 +56,8 @@ def get_user_mode_driver_version() -> tuple[int, ...]: A 2-tuple ``(MAJOR, MINOR)``, e.g. ``(13, 0)`` for CUDA 13.0. """ cdef int v - if CUDA_BINDINGS_NVML_IS_COMPATIBLE: - initialize() - v = nvml.system_get_cuda_driver_version() - else: - v = handle_return(driver.cuDriverGetVersion()) + initialize() + v = nvml.system_get_cuda_driver_version() return (v // 1000, (v // 10) % 100) @@ -91,10 +76,6 @@ def get_kernel_mode_driver_version() -> tuple[int, ...]: RuntimeError If the NVML library is not available. """ - if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: - raise RuntimeError( - "get_kernel_mode_driver_version requires NVML support" - ) initialize() return tuple(int(x) for x in nvml.system_get_driver_version().split(".")) @@ -108,8 +89,7 @@ def get_nvml_version() -> tuple[int, ...]: version: tuple[int, ...] Tuple of integers representing the NVML version components. """ - if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: - raise RuntimeError("NVML library is not available") + initialize() return tuple(int(v) for v in nvml.system_get_nvml_version().split(".")) @@ -122,8 +102,6 @@ def get_driver_branch() -> str: branch: str The driver branch string (e.g., ``"560"``, ``"open"``, etc.). """ - if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: - raise RuntimeError("NVML library is not available") initialize() return nvml.system_get_driver_branch() @@ -132,11 +110,8 @@ def get_num_devices() -> int: """ Return the number of devices in the system. """ - if CUDA_BINDINGS_NVML_IS_COMPATIBLE: - initialize() - return nvml.device_get_count_v2() - else: - return handle_return(runtime.cudaGetDeviceCount()) + initialize() + return nvml.device_get_count_v2() def get_process_name(pid: int) -> str: diff --git a/cuda_core/cuda/core/system/typing.py b/cuda_core/cuda/core/system/typing.py index 6ef9bcb2bc6..50e3356f8ff 100644 --- a/cuda_core/cuda/core/system/typing.py +++ b/cuda_core/cuda/core/system/typing.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +from cuda.bindings import nvml as _nvml +from cuda.bindings._internal._fast_enum import FastEnum as _FastEnum from cuda.core._utils.pycompat import StrEnum __all__ = [ @@ -12,8 +14,10 @@ "ClocksEventReasons", "CoolerControl", "CoolerTarget", + "DeviceArch", "EventType", "FanControlPolicy", + "FieldId", "GpuP2PCapsIndex", "GpuP2PStatus", "GpuTopologyLevel", @@ -320,44 +324,29 @@ class ThermalTarget(StrEnum): ThermalTarget.VCD_OUTLET.__doc__ = "Visual Computing Device Outlet temperature requires visual computing device handle." -# DeviceArch values are derived from cuda.bindings.nvml at definition time, so -# the class can only be defined when nvml is importable. -try: - from cuda.bindings import nvml as _nvml - - try: - from cuda.bindings._internal._fast_enum import FastEnum as _FastEnum - except ImportError: - from enum import IntEnum as _FastEnum - - # This uses FastEnum instead of StrEnum because the ordering of the values is - # meaningful, e.g. Kepler "or later" - class DeviceArch(_FastEnum): - """ - Device architecture. - """ - - KEPLER = int(_nvml.DeviceArch.KEPLER) - MAXWELL = int(_nvml.DeviceArch.MAXWELL) - PASCAL = int(_nvml.DeviceArch.PASCAL) - VOLTA = int(_nvml.DeviceArch.VOLTA) - TURING = int(_nvml.DeviceArch.TURING) - AMPERE = int(_nvml.DeviceArch.AMPERE) - ADA = int(_nvml.DeviceArch.ADA) - HOPPER = int(_nvml.DeviceArch.HOPPER) - BLACKWELL = int(_nvml.DeviceArch.BLACKWELL) - UNKNOWN = int(_nvml.DeviceArch.UNKNOWN) - - __all__.append("DeviceArch") +# DeviceArch values are derived from cuda.bindings.nvml at definition time. +# This uses FastEnum instead of StrEnum because the ordering of the values is +# meaningful, e.g. Kepler "or later" +class DeviceArch(_FastEnum): + """ + Device architecture. + """ - FieldId = _nvml.FieldId + KEPLER = int(_nvml.DeviceArch.KEPLER) + MAXWELL = int(_nvml.DeviceArch.MAXWELL) + PASCAL = int(_nvml.DeviceArch.PASCAL) + VOLTA = int(_nvml.DeviceArch.VOLTA) + TURING = int(_nvml.DeviceArch.TURING) + AMPERE = int(_nvml.DeviceArch.AMPERE) + ADA = int(_nvml.DeviceArch.ADA) + HOPPER = int(_nvml.DeviceArch.HOPPER) + BLACKWELL = int(_nvml.DeviceArch.BLACKWELL) + UNKNOWN = int(_nvml.DeviceArch.UNKNOWN) - __all__.append("FieldId") - del _nvml, _FastEnum +FieldId = _nvml.FieldId -except ImportError: - pass +del _nvml, _FastEnum del StrEnum diff --git a/cuda_core/docs/source/install.rst b/cuda_core/docs/source/install.rst index c048cfb2a2c..3fdb8f71cc4 100644 --- a/cuda_core/docs/source/install.rst +++ b/cuda_core/docs/source/install.rst @@ -45,7 +45,9 @@ Starting ``cuda-core`` 0.4.0, **experimental** packages for the `free-threaded i Installing from PyPI -------------------- -``cuda.core`` works with ``cuda.bindings`` (part of ``cuda-python``) 12 or 13. Test dependencies now use the ``cuda-toolkit`` metapackage for improved dependency resolution. For example with CUDA 12: +``cuda.core`` works with ``cuda-bindings`` (part of ``cuda-python``) 12 or 13, at or above the +release's per-major floor (see :ref:`cuda-core-bindings-floor`); the ``cu12`` and ``cu13`` extras +install a compatible version. Test dependencies now use the ``cuda-toolkit`` metapackage for improved dependency resolution. For example with CUDA 12: .. code-block:: console @@ -53,8 +55,9 @@ Installing from PyPI and likewise use ``[cu13]`` for CUDA 13. -Note that using ``cuda.core`` with NVRTC installed from PyPI via ``pip install`` requires -``cuda.bindings`` 12.8.0+. Likewise, with nvJitLink it requires 12.8.0+. +Upgrading ``cuda-core`` on its own can leave an older ``cuda-bindings`` installed than the new +release requires; ``import cuda.core`` then reports the required version and the ``pip`` command +that installs it. Installing from Conda (conda-forge) @@ -68,7 +71,8 @@ Same as above, ``cuda.core`` can be installed in a CUDA 12 or 13 environment. Fo and likewise use ``cuda-version=13`` for CUDA 13. -Note that to use ``cuda.core`` with nvJitLink installed from conda-forge requires ``cuda.bindings`` 12.8.0+. +The conda-forge package pins ``cuda-bindings`` to the version it was built against, so a +compatible ``cuda-bindings`` is installed alongside it. Development environment @@ -155,7 +159,12 @@ Installing from Source $ cd cuda-python/cuda_core $ pip install . -``cuda-bindings`` 12.x or 13.x is a required dependency. +A source build requires two things to agree (see :ref:`cuda-core-bindings-floor`): + +- ``cuda-bindings`` 12.x or 13.x at or above the release's floor for that major. An isolated + build (the default ``pip install``) installs it; other builds must provide it. +- A CUDA Toolkit, located through ``CUDA_PATH`` or ``CUDA_HOME``, whose ``cuda.h`` has the same + major.minor as that ``cuda-bindings``. The build fails early otherwise. .. note:: diff --git a/cuda_core/docs/source/release/1.3.0-notes.rst b/cuda_core/docs/source/release/1.3.0-notes.rst index 31598150775..9d47317c4f7 100644 --- a/cuda_core/docs/source/release/1.3.0-notes.rst +++ b/cuda_core/docs/source/release/1.3.0-notes.rst @@ -6,6 +6,32 @@ ``cuda.core`` 1.3.0 Release Notes ================================== +Breaking Changes +---------------- + +- ``cuda.core`` now requires a minimum ``cuda-bindings`` version per CUDA major, at build time and + at run time: 12.9.8 for CUDA 12 and 13.4.1 for CUDA 13 (see the + :ref:`support policy `). ``import cuda.core`` with an older + ``cuda-bindings`` fails with a message that names the required version and how to install it; + ``pip install cuda-core[cu12]`` / ``[cu13]`` installs a compatible version. A source build must + also use a ``cuda.h`` of the same major.minor as its ``cuda-bindings``; it fails early otherwise. + Supported CUDA drivers and CUDA Toolkit libraries are unchanged. Previously any + ``cuda-bindings`` of the right major was accepted, and an older one produced import errors for + missing C functions, silently disabled features, or crashes. + (https://github.com/NVIDIA/cuda-python/issues/2783) + +- With the ``cuda-bindings`` floor in place, whether a feature is available now depends on the + CUDA driver alone (and on the CUDA major of the ``cuda.core`` build). Checks that also inspected + the ``cuda-bindings`` version are gone, and the error messages they produced with them; error + messages that name a minimum now name a driver version. ``cuda.core.system`` always uses NVML + through ``cuda-bindings``, so ``cuda.core.system.CUDA_BINDINGS_NVML_IS_COMPATIBLE`` is always + ``True`` and is deprecated. :mod:`cuda.core.checkpoint` requires the CUDA 13 build of + ``cuda.core`` (it did in effect before: the CUDA 12 ``cuda-bindings`` lack a type it uses) and + now says so. The C++ layer calls the driver through the entry points ``cuda-bindings`` resolves + rather than through its Cython wrappers, so a driver function that the installed driver lacks + can no longer surface as ``SystemError`` from a C++ call. + (https://github.com/NVIDIA/cuda-python/issues/2783) + New features ------------ diff --git a/cuda_core/docs/source/support.rst b/cuda_core/docs/source/support.rst index 3a6548ce204..6117ea83328 100644 --- a/cuda_core/docs/source/support.rst +++ b/cuda_core/docs/source/support.rst @@ -45,7 +45,10 @@ CUDA Version Support example, ``cuda.core`` 1.x supports CUDA 12 and 13. In particular, what this entails is that all CUDA minor versions within the two major releases -(12.x, 13.x) are supported by the same ``cuda-core`` package. +(12.x, 13.x) are supported by the same ``cuda-core`` package, at run time: any CUDA driver and any +CUDA Toolkit libraries of a supported major work with the same ``cuda-core`` wheel. The one input +this does not extend to is ``cuda-bindings``, which has a per-release minimum (see +:ref:`cuda-core-bindings-floor` below). When a new CUDA major version is released and support for the oldest major version is dropped, ``cuda.core`` will release a new major version (e.g., 1.x → 2.0.0). @@ -59,8 +62,44 @@ When a new CUDA major version is released and support for the oldest major versi - 12, 13 As with any CUDA library, certain features may impose additional requirements on the minimum -``cuda-bindings``, CUDA library, or CUDA driver versions. Refer to the individual module -documentation for details. +CUDA library or CUDA driver versions. Refer to the individual module documentation for details. + +.. _cuda-core-bindings-floor: + +``cuda-bindings`` Version Requirements +************************************** + +Each ``cuda-core`` release declares, for each supported CUDA major version, a minimum +``cuda-bindings`` version, its *floor*: the newest ``cuda-bindings`` release of that major at the +time of the ``cuda-core`` release, which is the version the published wheels are built against. +The floors of the current release are recorded in ``cuda/core/_bindings_floor.py`` and in the +``cu12``/``cu13`` extras of ``cuda-core``. + +.. list-table:: ``cuda-bindings`` floors + :header-rows: 1 + + * - ``cuda-core`` version + - CUDA 12 + - CUDA 13 + * - 1.3.x + - ``cuda-bindings`` >= 12.9.8 + - ``cuda-bindings`` >= 13.4.1 + +- **At run time**, ``import cuda.core`` requires an installed ``cuda-bindings`` of the same major + as the ``cuda-core`` build in use and at least as new as that build's floor. An older + ``cuda-bindings`` fails at import with a message that names the version found, the version + required, and the ``pip`` command that fixes it. A newer ``cuda-bindings`` of the same major is + supported. +- **At build time**, a source build requires ``cuda-bindings`` at or above the floor and a + ``cuda.h`` (``CUDA_PATH`` or ``CUDA_HOME``) of the same major.minor as that ``cuda-bindings``, + which is the header ``cuda-bindings`` itself was generated from. Any other configuration fails + the build with a message that names what was found and what is required. Building against an + older CUDA Toolkit than the floor's minor is not supported. +- **The CUDA driver** is unaffected. Feature availability is decided by the driver alone: a + feature the installed driver lacks raises when it is used, as before. + +A floor is raised only in a release that needs a newer ``cuda-bindings`` API, and every such +change is listed under "Breaking Changes" in the :doc:`release notes `. Python Version Support ---------------------- diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 23a72a685f6..c21f81c5533 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -54,8 +54,8 @@ dependencies = [ ] [project.optional-dependencies] -cu12 = ["cuda-bindings[all]==12.*", "cuda-toolkit==12.*"] -cu13 = ["cuda-bindings[all]==13.*", "cuda-toolkit==13.*"] +cu12 = ["cuda-bindings[all]>=12.9.8,==12.*", "cuda-toolkit==12.*"] +cu13 = ["cuda-bindings[all]>=13.4.1,==13.*", "cuda-toolkit==13.*"] [dependency-groups] test = [ diff --git a/cuda_core/tests/graph/test_graph_builder.py b/cuda_core/tests/graph/test_graph_builder.py index b681e19de8a..8754823cac1 100644 --- a/cuda_core/tests/graph/test_graph_builder.py +++ b/cuda_core/tests/graph/test_graph_builder.py @@ -13,9 +13,7 @@ from cuda_python_test_helpers.marks import requires_module, skipif_need_cuda_headers from helpers.graph_kernels import compile_common_kernels, compile_conditional_kernels from helpers.misc import try_create_condition -from packaging.version import Version -import cuda.bindings from cuda.core import Device, LaunchConfig, LegacyPinnedMemoryResource, Program, ProgramOptions, StreamOptions, launch from cuda.core.graph import Graph, GraphBuilder, GraphCompleteOptions, GraphDefinition from cuda.core.graph._graph_builder import ( @@ -33,10 +31,10 @@ def _wait_until(predicate, timeout=5.0): def _skip_if_conditional_handles_unsupported(): - from cuda.core._utils.version import binding_version, driver_version + from cuda.core._utils.version import driver_version - if driver_version() < (12, 3, 0) or binding_version() < (12, 3, 0): - pytest.skip("conditional handles require CUDA driver and bindings 12.3+") + if driver_version() < (12, 3, 0): + pytest.skip("conditional handles require CUDA driver 12.3+") def test_graph_is_building(init_cuda): @@ -781,13 +779,6 @@ def _assert_programmatic_dependency_edge(graph_definition): """ from cuda.bindings import driver - # cuda.bindings before 13.3.0 (before 12.9.7 on the 12.x branch) returned - # CUgraphEdgeData wrappers backed by a scratch buffer that was freed before the - # call returned, so every field reads back as freed heap memory (#1804). - version = Version(cuda.bindings.__version__) - if version < Version("13.3.0" if version.major >= 13 else "12.9.7"): - pytest.skip(f"cuda.bindings {version} returns dangling graph edge data (#1804)") - h_graph = graph_definition.handle if driver.CUDA_VERSION >= 13000: get_edges = driver.cuGraphGetEdges diff --git a/cuda_core/tests/graph/test_graph_definition.py b/cuda_core/tests/graph/test_graph_definition.py index 4444bafc5de..551b2fa5a3e 100644 --- a/cuda_core/tests/graph/test_graph_definition.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -54,18 +54,18 @@ def _skip_if_no_managed_mempool(): def _has_node_get_params(): - from cuda.core._utils.version import binding_version, driver_version + from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version - return driver_version() >= (13, 2, 0) and binding_version() >= (13, 2, 0) + return BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 2, 0) _HAS_NODE_GET_PARAMS = _has_node_get_params() def _bindings_major_version(): - from cuda.core._utils.version import binding_version + from cuda.core._utils.version import BUILD_CUDA_MAJOR - return binding_version()[0] + return BUILD_CUDA_MAJOR _BINDINGS_MAJOR = _bindings_major_version() @@ -471,7 +471,7 @@ def _build_switch_node(g): pytest.param( NodeSpec("alloc_managed", AllocNode, "CU_GRAPH_NODE_TYPE_MEM_ALLOC", _build_alloc_managed_node), id="alloc_managed", - marks=pytest.mark.skipif(_BINDINGS_MAJOR < 13, reason="managed alloc requires CUDA 13.0+ bindings"), + marks=pytest.mark.skipif(_BINDINGS_MAJOR < 13, reason="managed alloc requires the CUDA 13 build"), ), pytest.param(NodeSpec("free", FreeNode, "CU_GRAPH_NODE_TYPE_MEM_FREE", _build_free_node), id="free"), pytest.param(NodeSpec("memset", MemsetNode, "CU_GRAPH_NODE_TYPE_MEMSET", _build_memset_node), id="memset"), diff --git a/cuda_core/tests/memory/test_copy_batch_options.py b/cuda_core/tests/memory/test_copy_batch_options.py index 85dbe65e3c4..b6fbdc8b0df 100644 --- a/cuda_core/tests/memory/test_copy_batch_options.py +++ b/cuda_core/tests/memory/test_copy_batch_options.py @@ -25,7 +25,7 @@ _normalize_copy_options, ) from cuda.core._stream import PER_THREAD_DEFAULT_STREAM -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version from cuda.core.utils import ( CopyOptions, MemcpyOverlapMode, @@ -36,7 +36,7 @@ def _batch_native_available(): """True when copy_batch will actually use cuMemcpyBatchAsync.""" - return binding_version() >= (13, 0, 0) and driver_version() >= (13, 0, 0) + return BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0) class TestOptionsEncoding: diff --git a/cuda_core/tests/memory/test_copy_single_options.py b/cuda_core/tests/memory/test_copy_single_options.py index c1ce9024291..3dc17ebd4a6 100644 --- a/cuda_core/tests/memory/test_copy_single_options.py +++ b/cuda_core/tests/memory/test_copy_single_options.py @@ -10,7 +10,7 @@ from cuda.core import Device, Host, LegacyPinnedMemoryResource from cuda.core._stream import LEGACY_DEFAULT_STREAM, PER_THREAD_DEFAULT_STREAM -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version from cuda.core.utils import CopyOptions, MemcpyOverlapMode, MemcpySrcAccessOrder SIZE = 4096 @@ -19,12 +19,12 @@ def _options_honored(): """True when cuMemcpyWithAttributesAsync will actually be used for options. - Mirrors _with_attributes_available() in _buffer.pyx. CI runs a matrix - that includes pre-CUDA-13.2 driver/bindings combinations (see + Mirrors _with_attributes_available() in _copy_attributes.pxd. CI runs a + matrix that includes CUDA 12 builds and pre-13.2 drivers (see ci/test-matrix.yml), where this is False and the DURING_API_CALL tests below must expect a RuntimeError instead of a successful copy. """ - return driver_version() >= (13, 2, 0) and binding_version() >= (13, 2, 0) + return BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 2, 0) @pytest.fixture diff --git a/cuda_core/tests/memory/test_managed_ops.py b/cuda_core/tests/memory/test_managed_ops.py index 16e6a8f181f..6915d261e1d 100644 --- a/cuda_core/tests/memory/test_managed_ops.py +++ b/cuda_core/tests/memory/test_managed_ops.py @@ -7,9 +7,8 @@ from helpers.buffers import DummyDeviceMemoryResource, DummyUnifiedMemoryResource from helpers.memory import create_managed_memory_resource_or_skip -from cuda.bindings import driver from cuda.core import Device, Host, ManagedBuffer -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version # Managed-memory prefetch and CU_MEM_RANGE_ATTRIBUTE_LAST_PREFETCH_LOCATION # operate at physical-page granularity. Test buffers must each occupy a full @@ -52,8 +51,8 @@ def _skip_if_managed_location_ops_unsupported(device): def _skip_if_managed_discard_prefetch_unsupported(device): _skip_if_managed_location_ops_unsupported(device) - if not hasattr(driver, "cuMemDiscardAndPrefetchBatchAsync"): - pytest.skip("discard-prefetch requires cuda.bindings support") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("discard-prefetch requires the CUDA 13 build") visible_devices = Device.get_all_devices() if not all(dev.properties.concurrent_managed_access for dev in visible_devices): @@ -161,20 +160,18 @@ def test_host_passthrough(self): def test_host_numa_passthrough(self): from cuda.core._memory._managed_location import _coerce_location - from cuda.core._utils.version import binding_version - if binding_version() < (13, 0, 0): - pytest.skip("Host(numa_id=N) requires CUDA 13 bindings") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("Host(numa_id=N) requires the CUDA 13 build") spec = _coerce_location(Host(numa_id=3)) assert spec.kind == "host_numa" assert spec.id == 3 def test_host_numa_current_passthrough(self): from cuda.core._memory._managed_location import _coerce_location - from cuda.core._utils.version import binding_version - if binding_version() < (13, 0, 0): - pytest.skip("Host.numa_current() requires CUDA 13 bindings") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("Host.numa_current() requires the CUDA 13 build") spec = _coerce_location(Host.numa_current()) assert spec.kind == "host_numa_current" @@ -246,8 +243,8 @@ class TestDiscardBatch: def test_basic(self, location_ops_device, location_ops_mr): from cuda.core.utils import discard_batch, prefetch_batch - if not hasattr(driver, "cuMemDiscardBatchAsync"): - pytest.skip("cuMemDiscardBatchAsync unavailable") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("cuMemDiscardBatchAsync requires the CUDA 13 build") device = location_ops_device stream = device.create_stream() bufs = [location_ops_mr.allocate(_MANAGED_TEST_ALLOCATION_SIZE, stream=stream) for _ in range(3)] @@ -265,8 +262,8 @@ class TestDiscardPrefetchBatch: def test_same_location(self, location_ops_device, location_ops_mr): from cuda.core.utils import discard_prefetch_batch, prefetch_batch - if not hasattr(driver, "cuMemDiscardAndPrefetchBatchAsync"): - pytest.skip("cuMemDiscardAndPrefetchBatchAsync unavailable") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("cuMemDiscardAndPrefetchBatchAsync requires the CUDA 13 build") device = location_ops_device stream = device.create_stream() bufs = [location_ops_mr.allocate(_MANAGED_TEST_ALLOCATION_SIZE, stream=stream) for _ in range(2)] @@ -360,8 +357,8 @@ def test_last_prefetch_location_initially_none(self, external_managed_buffer): assert external_managed_buffer.last_prefetch_location is None @pytest.mark.skipif( - binding_version() < (13, 0, 0) or driver_version() < (13, 0, 0), - reason="Host NUMA last-prefetch location requires CUDA 13", + BUILD_CUDA_MAJOR < 13 or driver_version() < (13, 0, 0), + reason="Host NUMA last-prefetch location requires the CUDA 13 build and driver", ) @pytest.mark.agent_authored(model="gpt-5") def test_last_prefetch_location_roundtrip_host_numa(self, location_ops_device, managed_buffer): @@ -398,10 +395,8 @@ def test_preferred_location_roundtrip(self, location_ops_device, external_manage @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 - - if binding_version() < (13, 0, 0): - pytest.skip("Host(numa_id=N) round-trip requires CUDA 13 bindings") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("Host(numa_id=N) round-trip requires the CUDA 13 build") plain = DummyUnifiedMemoryResource(location_ops_device).allocate(_MANAGED_TEST_ALLOCATION_SIZE) try: buf = ManagedBuffer.from_handle(plain.handle, plain.size, owner=plain) @@ -470,7 +465,7 @@ def test_accessed_by_set_assignment_validates_kind_before_mutation( with pytest.raises( (ValueError, TypeError), - match=r"does not support location_type='host_numa'|cuda-bindings 13\.0\+", + match=r"does not support location_type='host_numa'|CUDA 13 build of cuda\.core", ): buf.accessed_by = {device, Host(numa_id=0)} @@ -486,8 +481,8 @@ def test_instance_prefetch(self, location_ops_device, managed_buffer): assert buf.last_prefetch_location == device def test_instance_discard(self, location_ops_device, managed_buffer): - if not hasattr(driver, "cuMemDiscardBatchAsync"): - pytest.skip("cuMemDiscardBatchAsync unavailable") + if BUILD_CUDA_MAJOR < 13: + pytest.skip("cuMemDiscardBatchAsync requires the CUDA 13 build") device = location_ops_device buf = managed_buffer stream = device.create_stream() @@ -522,7 +517,7 @@ def test_operation_validation(self, managed_buffer): # rejected at the boundary first (TypeError). with pytest.raises( (ValueError, TypeError), - match=r"does not support location_type='host_numa'|cuda-bindings 13\.0\+", + match=r"does not support location_type='host_numa'|CUDA 13 build of cuda\.core", ): buf.accessed_by.add(Host(numa_id=_INVALID_HOST_DEVICE_ORDINAL)) @@ -541,13 +536,13 @@ def test_advise_location_validation(self, location_ops_device, external_managed_ # accessed_by rejects host_numa (CUDA 13: kind check; CUDA 12: boundary) with pytest.raises( (ValueError, TypeError), - match=r"does not support location_type='host_numa'|cuda-bindings 13\.0\+", + match=r"does not support location_type='host_numa'|CUDA 13 build of cuda\.core", ): buf.accessed_by.add(Host(numa_id=0)) # accessed_by rejects host_numa_current (same reasoning) with pytest.raises( (ValueError, TypeError), - match=r"does not support location_type='host_numa_current'|cuda-bindings 13\.0\+", + match=r"does not support location_type='host_numa_current'|CUDA 13 build of cuda\.core", ): buf.accessed_by.add(Host.numa_current()) diff --git a/cuda_core/tests/system/test_system_device.py b/cuda_core/tests/system/test_system_device.py index b52a0cec6d7..841424e7caa 100644 --- a/cuda_core/tests/system/test_system_device.py +++ b/cuda_core/tests/system/test_system_device.py @@ -15,19 +15,16 @@ import helpers import pytest +from cuda.bindings import nvml +from cuda.bindings.nvml import DeviceArch from cuda.core import Device as CudaDevice from cuda.core import system -from cuda.core.system import typing - -if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: - from cuda.bindings import nvml - from cuda.bindings.nvml import DeviceArch - from cuda.core.system import _device +from cuda.core.system import _device, typing @pytest.fixture(autouse=True, scope="module") def check_gpu_available(): - if not system.CUDA_BINDINGS_NVML_IS_COMPATIBLE or system.get_num_devices() == 0: + if system.get_num_devices() == 0: pytest.skip("No GPUs available to run device tests", allow_module_level=True) diff --git a/cuda_core/tests/system/test_system_events.py b/cuda_core/tests/system/test_system_events.py index e8b218325cc..9f91e03675b 100644 --- a/cuda_core/tests/system/test_system_events.py +++ b/cuda_core/tests/system/test_system_events.py @@ -10,13 +10,11 @@ import helpers import pytest +from cuda.bindings import nvml from cuda.core import Device as CudaDevice from cuda.core import system from cuda.core.system import typing - -if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: - from cuda.bindings import nvml - from cuda.core.system._system_events import SystemEvent, SystemEvents, _pci_bus_id_from_gpu_id +from cuda.core.system._system_events import SystemEvent, SystemEvents, _pci_bus_id_from_gpu_id @pytest.mark.agent_authored(model="claude-opus-4.7") diff --git a/cuda_core/tests/system/test_system_system.py b/cuda_core/tests/system/test_system_system.py index 9fc600b05da..fca171a28b0 100644 --- a/cuda_core/tests/system/test_system_system.py +++ b/cuda_core/tests/system/test_system_system.py @@ -35,13 +35,6 @@ def test_kernel_mode_driver_version(): assert 0 <= ver_patch[0] <= 99 -def test_kernel_mode_driver_version_requires_nvml(): - if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: - pytest.skip("NVML is available, cannot test the error path") - with pytest.raises(RuntimeError, match="requires NVML support"): - system.get_kernel_mode_driver_version() - - @skip_if_nvml_unsupported def test_nvml_version(): nvml_version = system.get_nvml_version() diff --git a/cuda_core/tests/test_bindings_floor.py b/cuda_core/tests/test_bindings_floor.py new file mode 100644 index 00000000000..d01700d63ec --- /dev/null +++ b/cuda_core/tests/test_bindings_floor.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""The cuda-bindings version floor (cuda/core/_bindings_floor.py) and the +import-time check built on it. + +Source-tree properties and pure functions only: no GPU, so this file also runs +with --noconftest (conftest.py initializes CUDA). The consistency tests read +pyproject.toml and ci/versions.yml from the checkout, so they need the source +tree next to the tests, which every CI job that runs tests/ has. + + pytest tests/test_bindings_floor.py -v --noconftest +""" + +import re +from pathlib import Path + +import pytest + +from cuda.core import _bindings_floor as floor_mod +from cuda.core._bindings_floor import ( + CUDA_BINDINGS_FLOOR, + SUPPORTED_CUDA_MAJORS, + check_installed_bindings, + cuda_version_of, + format_version, + pip_requirement, + release_triple, + required_minimum, +) + +CUDA_CORE = Path(__file__).resolve().parent.parent +REPO = CUDA_CORE.parent + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_floor_module_is_import_free(): + """build_hooks.py loads it by file path during the build; it must stay standard-library only.""" + source = Path(floor_mod.__file__).read_text(encoding="utf-8") + imports = re.findall(r"^\s*(?:from|import)\s+(\w+)", source, re.M) + assert set(imports) <= {"__future__", "re"} + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_floors_are_release_triples_of_their_major(): + assert SUPPORTED_CUDA_MAJORS == (12, 13) + for major, floor in CUDA_BINDINGS_FLOOR.items(): + assert len(floor) == 3 + assert floor[0] == major + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +@pytest.mark.parametrize( + ("version", "expected"), + [ + ("13.4.1", (13, 4, 1)), + ("13.4.1a0", (13, 4, 1)), + ("13.4.2.dev249+g471618971c2", (13, 4, 2)), + ("12.9.8", (12, 9, 8)), + (" 12.9.9.dev2 ", (12, 9, 9)), + ("0.1.dev1+g0d22cb444", None), # shallow clone + ("13.4", None), + ("", None), + ], +) +def test_release_triple(version, expected): + assert release_triple(version) == expected + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_formatting_helpers(): + assert format_version((13, 4, 1)) == "13.4.1" + assert cuda_version_of((13, 4, 1)) == 13040 + assert cuda_version_of((12, 9, 8)) == 12090 + assert pip_requirement(13) == f"cuda-bindings>={format_version(CUDA_BINDINGS_FLOOR[13])},==13.*" + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_required_minimum_is_the_floor_or_the_header_minor(): + floor = CUDA_BINDINGS_FLOOR[13] + header_at_floor = cuda_version_of(floor) + assert required_minimum(13, header_at_floor) == floor + # A build against a newer header than the floor's minor demands that minor: + # the driver-pointer keys are derived from the header's macros. + assert required_minimum(13, header_at_floor + 10) == (13, floor[1] + 1, 0) + # An older header cannot win over the floor. + assert required_minimum(13, 13000) == floor + + +class TestCheckInstalledBindings: + FLOOR = CUDA_BINDINGS_FLOOR[13] + HEADER = cuda_version_of(FLOOR) + + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize( + "installed", + [ + format_version(FLOOR), + f"{FLOOR[0]}.{FLOOR[1]}.{FLOOR[2] + 1}", + f"{FLOOR[0]}.{FLOOR[1]}.{FLOOR[2] + 1}.dev249+gabcdef0", # main-built bindings in CI + f"{FLOOR[0]}.{FLOOR[1] + 1}.0b1", # newer bindings than the build: supported + ], + ) + def test_accepts_the_floor_and_newer(self, installed): + assert check_installed_bindings(installed, 13, self.HEADER, "1.3.0") == release_triple(installed) + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_rejects_older_than_the_floor_with_the_fix(self): + older = f"{self.FLOOR[0]}.{self.FLOOR[1] - 1}.1" + with pytest.raises(ImportError) as excinfo: + check_installed_bindings(older, 13, self.HEADER, "1.3.0") + message = str(excinfo.value) + assert f"requires cuda-bindings >= {format_version(self.FLOOR)} for CUDA 13" in message + assert f"(found {older})" in message + assert f"pip install -U 'cuda-bindings>={format_version(self.FLOOR)},==13.*'" in message + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_rejects_a_minor_older_than_the_header(self): + # Built against a header one minor above the floor; the floor itself no longer suffices. + header = self.HEADER + 10 + with pytest.raises(ImportError, match=rf"requires cuda-bindings >= 13\.{self.FLOOR[1] + 1}\.0"): + check_installed_bindings(format_version(self.FLOOR), 13, header, "1.3.0") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_rejects_another_major_than_the_build(self): + with pytest.raises(ImportError, match="build is for CUDA 12, but the installed cuda-bindings is 13.4.1"): + check_installed_bindings("13.4.1", 12, 12090, "1.3.0") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize("installed", ["0.1.dev1+g0d22cb444", "11.8.0", "14.0.0", "garbage"]) + def test_rejects_unsupported_or_unparseable_versions(self, installed): + with pytest.raises( + ImportError, match=rf"cuda-bindings 12\.x or 13\.x must be installed \(found {re.escape(installed)}\)" + ): + check_installed_bindings(installed, 13, self.HEADER, "1.3.0") + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_pyproject_extras_pin_the_floor(): + """The static `cu12`/`cu13` extras cannot read the module; keep them in step by test.""" + pyproject = (CUDA_CORE / "pyproject.toml").read_text(encoding="utf-8") + for major in SUPPORTED_CUDA_MAJORS: + m = re.search(rf'^cu{major} = \["cuda-bindings\[all\]([^"]+)"', pyproject, re.M) + assert m, f"no cu{major} extra in pyproject.toml" + assert m.group(1) == pip_requirement(major).removeprefix("cuda-bindings") + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_ci_toolkit_pins_match_the_floors_minor(): + """CI builds each major against the toolkit pinned in ci/versions.yml; the + build requires that header's major.minor to equal the bindings', so the + floor of each major must sit in the same minor as its toolkit pin.""" + versions = (REPO / "ci" / "versions.yml").read_text(encoding="utf-8") + pins = dict(re.findall(r"^\s+(build|prev_build):\s*\n\s+version:\s*\"(\d+\.\d+)", versions, re.M)) + assert set(pins) == {"build", "prev_build"}, pins + by_major = {int(v.split(".")[0]): v for v in pins.values()} + for major, floor in CUDA_BINDINGS_FLOOR.items(): + assert by_major[major] == f"{floor[0]}.{floor[1]}", ( + f"ci/versions.yml builds CUDA {major} against {by_major[major]} but the floor is {format_version(floor)}" + ) diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 27c057e0297..3d9d4fc8541 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -232,6 +232,9 @@ def fake_cythonize(ext_modules, **kwargs): # Builds resolve the CTK for include dirs; stub it so the test runs # where no toolkit is installed (e.g. the wheels CI jobs). monkeypatch.setattr(build_hooks, "_get_cuda_path", lambda: "/nonexistent-cuda") + # The configuration check reads that header and the installed cuda-bindings; + # it has its own tests (TestBuildConfigurationCheck). + monkeypatch.setattr(build_hooks, "_check_build_configuration", lambda *_: None) monkeypatch.setattr(build_hooks, "cythonize", fake_cythonize) monkeypatch.setenv("CUDA_CORE_BUILD_MAJOR", cuda_major) build_hooks._determine_cuda_major_version.cache_clear() @@ -451,3 +454,160 @@ def test_serial_builds_and_compilers_without_the_hook_keep_the_stock_path(self, cmd = self._build_ext(monkeypatch, 4, self.MsvcLikeCompiler()) with cmd._parallel_source_compilation(): assert cmd.compiler.compile(["a.cpp"]) == "stock" + + +def _fake_bindings(monkeypatch, version): + """Make the build see an installed cuda-bindings of `version` (None: not installed).""" + import types + + def import_cuda_bindings(): + if version is None: + raise ModuleNotFoundError("No module named 'cuda.bindings'", name="cuda.bindings") + module = types.ModuleType("cuda.bindings") + module.__version__ = version + return module + + monkeypatch.setattr(build_hooks, "_import_cuda_bindings", import_cuda_bindings) + + +def _write_cuda_h(tmp_path, cuda_version): + include = tmp_path / "include" + include.mkdir(exist_ok=True) + (include / "cuda.h").write_text(f"#define CUDA_VERSION {cuda_version}\n") + return str(tmp_path) + + +class TestBuildConfigurationCheck: + """_check_build_configuration() accepts exactly one configuration per CUDA + major: cuda-bindings at or above the floor, and a cuda.h of the same + major.minor as that cuda-bindings. Anything else is a build error that + names what was found and what is required.""" + + FLOOR = build_hooks._load_bindings_floor().CUDA_BINDINGS_FLOOR + + @pytest.fixture(autouse=True) + def _isolate_build_info(self, tmp_path, monkeypatch): + monkeypatch.setattr(build_hooks, "_BUILD_INFO_PATH", tmp_path / "_build_info.py") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize("major", [12, 13]) + def test_floor_bindings_and_matching_header_pass_and_are_recorded(self, tmp_path, monkeypatch, major): + floor = self.FLOOR[major] + version = f"{floor[0]}.{floor[1]}.{floor[2] + 1}.dev3+gabcdef0" + _fake_bindings(monkeypatch, version) + cuda_path = _write_cuda_h(tmp_path, floor[0] * 1000 + floor[1] * 10) + + build_hooks._check_build_configuration(cuda_path, str(major)) + + spec = importlib.util.spec_from_file_location("_build_info_under_test", build_hooks._BUILD_INFO_PATH) + info = importlib.util.module_from_spec(spec) + spec.loader.exec_module(info) + assert major == info.CUDA_MAJOR + assert floor[0] * 1000 + floor[1] * 10 == info.CUDA_VERSION + assert floor == info.CUDA_BINDINGS_FLOOR + assert version == info.CUDA_BINDINGS_BUILD_VERSION + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_bindings_below_the_floor_fail(self, tmp_path, monkeypatch): + floor = self.FLOOR[13] + _fake_bindings(monkeypatch, f"{floor[0]}.{floor[1]}.{floor[2] - 1}" if floor[2] else "13.0.0") + cuda_path = _write_cuda_h(tmp_path, 13040) + with pytest.raises(RuntimeError, match=r"requires cuda-bindings >= 13\.\d+\.\d+ for CUDA 13"): + build_hooks._check_build_configuration(cuda_path, "13") + assert not build_hooks._BUILD_INFO_PATH.exists() + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_bindings_of_another_major_fail(self, tmp_path, monkeypatch): + _fake_bindings(monkeypatch, "13.4.1") + cuda_path = _write_cuda_h(tmp_path, 12090) + with pytest.raises( + RuntimeError, match="Building cuda.core for CUDA 12, but the installed cuda-bindings is 13.4.1" + ): + build_hooks._check_build_configuration(cuda_path, "12") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize("header", [13030, 13050, 12090]) + def test_header_minor_must_match_bindings(self, tmp_path, monkeypatch, header): + _fake_bindings(monkeypatch, "13.4.1") + cuda_path = _write_cuda_h(tmp_path, header) + with pytest.raises(RuntimeError, match="same major.minor as its cuda-bindings"): + build_hooks._check_build_configuration(cuda_path, "13") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_header_is_read_even_when_the_major_override_is_set(self, tmp_path, monkeypatch): + # CUDA_CORE_BUILD_MAJOR skips header detection of the major, not this check. + monkeypatch.setenv("CUDA_CORE_BUILD_MAJOR", "13") + _fake_bindings(monkeypatch, "13.4.1") + cuda_path = _write_cuda_h(tmp_path, 13030) + with pytest.raises(RuntimeError, match="same major.minor"): + build_hooks._check_build_configuration(cuda_path, "13") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_missing_bindings_is_a_build_error(self, tmp_path, monkeypatch): + _fake_bindings(monkeypatch, None) # no cuda-bindings in the build environment + cuda_path = _write_cuda_h(tmp_path, 13040) + with pytest.raises(RuntimeError, match="requires cuda-bindings to build"): + build_hooks._check_build_configuration(cuda_path, "13") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_unparseable_bindings_version_is_a_build_error(self, tmp_path, monkeypatch): + _fake_bindings(monkeypatch, "0.1.dev1+g0d22cb444") # a shallow clone of cuda-bindings + cuda_path = _write_cuda_h(tmp_path, 13040) + with pytest.raises(RuntimeError, match="Cannot parse the installed cuda-bindings version"): + build_hooks._check_build_configuration(cuda_path, "13") + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_unsupported_major_is_a_build_error(self, tmp_path, monkeypatch): + _fake_bindings(monkeypatch, "14.0.0") + cuda_path = _write_cuda_h(tmp_path, 14000) + with pytest.raises(RuntimeError, match="does not support CUDA 14"): + build_hooks._check_build_configuration(cuda_path, "14") + + +class TestBuildRequirement: + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize("major", ["12", "13"]) + def test_pins_the_floor_and_the_major(self, monkeypatch, major): + monkeypatch.setenv("CUDA_CORE_BUILD_MAJOR", major) + build_hooks._determine_cuda_major_version.cache_clear() + floor = build_hooks._load_bindings_floor().CUDA_BINDINGS_FLOOR[int(major)] + (requirement,) = build_hooks._get_cuda_bindings_require() + assert requirement == f"cuda-bindings>={floor[0]}.{floor[1]}.{floor[2]},=={major}.*" + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_unsupported_major_names_the_supported_ones(self, monkeypatch): + monkeypatch.setenv("CUDA_CORE_BUILD_MAJOR", "11") + build_hooks._determine_cuda_major_version.cache_clear() + with pytest.raises(RuntimeError, match="does not support CUDA 11.*12, 13"): + build_hooks._get_cuda_bindings_require() + + +class TestDefineMacros: + """The C++ learns the build decision through two macros (see _cpp/rt/versions.hpp).""" + + @pytest.mark.agent_authored(model="claude-fable-5-1") + @pytest.mark.parametrize("major", ["12", "13"]) + def test_major_and_floor_header_version(self, major): + floor = build_hooks._load_bindings_floor().CUDA_BINDINGS_FLOOR[int(major)] + assert build_hooks._build_define_macros(major) == [ + ("CUDA_CORE_BUILD_MAJOR", major), + ("CUDA_CORE_MIN_CUDA_VERSION", str(floor[0] * 1000 + floor[1] * 10)), + ] + + @pytest.mark.agent_authored(model="claude-fable-5-1") + def test_extensions_receive_the_macros(self, monkeypatch): + captured = {} + + def fake_cythonize(ext_modules, **kwargs): + captured["macros"] = {tuple(ext.define_macros) for ext in ext_modules} + return [] + + monkeypatch.setattr(build_hooks, "_get_cuda_path", lambda: "/nonexistent-cuda") + monkeypatch.setattr(build_hooks, "_check_build_configuration", lambda *_: None) + monkeypatch.setattr(build_hooks, "cythonize", fake_cythonize) + monkeypatch.setenv("CUDA_CORE_BUILD_MAJOR", "13") + build_hooks._determine_cuda_major_version.cache_clear() + monkeypatch.chdir(Path(__file__).parent.parent) + monkeypatch.setattr(sys, "path", list(sys.path)) + build_hooks._build_cuda_core() + assert captured["macros"] == {tuple(build_hooks._build_define_macros("13"))} diff --git a/cuda_core/tests/test_checkpoint.py b/cuda_core/tests/test_checkpoint.py index ff727eb9fff..d2ce7491532 100644 --- a/cuda_core/tests/test_checkpoint.py +++ b/cuda_core/tests/test_checkpoint.py @@ -40,18 +40,11 @@ def _checkpoint_available(): def _checkpoint_unavailable_can_skip(message): - if message.startswith( + return message.startswith( ( "CUDA checkpointing is not supported by the installed NVIDIA driver.", - "CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. Found cuda.bindings ", + "CUDA checkpointing requires the CUDA 13 build of cuda.core", ) - ): - return True - - return ( - checkpoint._binding_version()[0] == 12 - and message - == "CUDA checkpointing requires cuda.bindings with CUDA checkpoint API support. Missing: CUcheckpointGpuPair" ) @@ -414,14 +407,12 @@ def test_pid_is_read_only(self): import ctypes from cuda.bindings import driver as _bindings_driver +from cuda.core._utils.version import BUILD_CUDA_MAJOR -# The checkpoint functions, structs, and enums are generated and shipped -# together from the same CUDA headers, so probe them as one atomic API surface. -_HAS_CHECKPOINT_BINDINGS = all(hasattr(_bindings_driver, name) for name in checkpoint._REQUIRED_BINDING_ATTRS) - +# The helpers build CUcheckpointGpuPair, a CUDA 13 type the CUDA 12 bindings lack. needs_checkpoint_bindings = pytest.mark.skipif( - not _HAS_CHECKPOINT_BINDINGS, - reason="cuda.bindings does not expose the CUDA checkpoint API", + BUILD_CUDA_MAJOR < 13, + reason="the checkpoint helpers use CUDA 13 binding types", ) diff --git a/cuda_core/tests/test_cuda_utils.py b/cuda_core/tests/test_cuda_utils.py index 32ea504248d..3f8268b71f3 100644 --- a/cuda_core/tests/test_cuda_utils.py +++ b/cuda_core/tests/test_cuda_utils.py @@ -11,14 +11,6 @@ from cuda.core._utils.clear_error_support import assert_type_str_or_bytes_like, raise_code_path_meant_to_be_unreachable -def _skip_if_bindings_pre_enum_docstrings(): - from cuda.core._utils.enum_explanations_helpers import _binding_version_has_usable_enum_docstrings - from cuda.core._utils.version import binding_version - - if not _binding_version_has_usable_enum_docstrings(binding_version()): - pytest.skip("cuda-bindings version does not expose usable enum __doc__ strings") - - def _assert_cleanup_example_matches_or_xfail(actual, expected): # Pin a few real cleanup-sensitive enum docs. If one starts failing, review # the raw ``__doc__`` and today's cleaned output: either update the expected @@ -59,7 +51,6 @@ def test_check_runtime_error(): def test_driver_error_enum_has_non_empty_docstring(): - _skip_if_bindings_pre_enum_docstrings() doc = driver.CUresult.CUDA_ERROR_INVALID_VALUE.__doc__ assert doc is not None @@ -67,7 +58,6 @@ def test_driver_error_enum_has_non_empty_docstring(): def test_runtime_error_enum_has_non_empty_docstring(): - _skip_if_bindings_pre_enum_docstrings() doc = runtime.cudaError_t.cudaErrorInvalidValue.__doc__ assert doc is not None @@ -131,7 +121,6 @@ def test_runtime_error_enum_has_non_empty_docstring(): ], ) def test_enum_doc_cleanup_examples_are_reviewed_on_change(explanations, error, expected): - _skip_if_bindings_pre_enum_docstrings() actual = explanations.get(int(error)) _assert_cleanup_example_matches_or_xfail(actual, expected) diff --git a/cuda_core/tests/test_device.py b/cuda_core/tests/test_device.py index 6b3aca9dc73..de72888f757 100644 --- a/cuda_core/tests/test_device.py +++ b/cuda_core/tests/test_device.py @@ -27,15 +27,9 @@ def test_device_init_disabled(): def test_to_system_device(deinit_cuda): - from cuda.core.system import _system device = Device() - if not _system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: - with pytest.raises(RuntimeError): - device.to_system_device() - pytest.skip("NVML support requires cuda.bindings version 12.9.6+ for CUDA 12.x or 13.2.0+ for CUDA 13.x") - from cuda_python_test_helpers.arch_check import hardware_supports_nvml if not hardware_supports_nvml(): diff --git a/cuda_core/tests/test_enum_coverage.py b/cuda_core/tests/test_enum_coverage.py index 119554c226f..d38a5b15d00 100644 --- a/cuda_core/tests/test_enum_coverage.py +++ b/cuda_core/tests/test_enum_coverage.py @@ -14,9 +14,11 @@ import pytest import cuda.core +import cuda.core.system.typing as system_typing import cuda.core.typing -from cuda.bindings import driver -from cuda.core import system +from cuda.bindings import driver, nvml +from cuda.core._utils.version import BUILD_CUDA_MAJOR +from cuda.core.system import _device, _system_events if sys.version_info >= (3, 11): from enum import StrEnum @@ -123,175 +125,168 @@ ), ] -if system.CUDA_BINDINGS_NVML_IS_COMPATIBLE: - # Populated below only when NVML bindings are compatible, so that importing - # this module on an incompatible host does not raise ImportError. - import cuda.core.system.typing as system_typing - from cuda.bindings import nvml - from cuda.core.system import _device, _system_events +_MODULES.append(system_typing) - _MODULES.append(system_typing) - - _CLOCKS_EVENT_REASONS_STR_UNMAPPED = { - core_member - for binding_member, core_member in ( - ("EVENT_REASON_BOARD_LIMIT", "BOARD_LIMIT"), - ("EVENT_REASON_RELIABILITY", "RELIABILITY"), - ) - if binding_member not in nvml.ClocksEventReasons.__members__ - } - - _CASES.extend( - [ - ( - nvml.DeviceAddressingModeType, - system_typing.AddressingMode, - _device._ADDRESSING_MODE_MAPPING, - # NONE means "no special addressing mode is active"; not a valid target - {"DEVICE_ADDRESSING_MODE_NONE"}, - set(), - ), - ( - nvml.BrandType, - None, # maps to plain str, not a StrEnum - _device._BRAND_TYPE_MAPPING, - # COUNT is a sentinel, not a real brand - {"BRAND_COUNT"}, - set(), - ), - ( - nvml.GpuP2PStatus, - system_typing.GpuP2PStatus, - _device._GPU_P2P_STATUS_MAPPING, - # Both the typo'd (SUPPORED) and corrected (SUPPORTED) spellings - # share the same integer value; the mapping covers both via aliases - {"P2P_STATUS_CHIPSET_NOT_SUPPORED"}, - set(), - ), - ( - nvml.ClocksEventReasons, - system_typing.ClocksEventReasons, - _device._CLOCKS_EVENT_REASONS_MAPPING, - set(), - _CLOCKS_EVENT_REASONS_STR_UNMAPPED, - ), - ( - nvml.EventType, - system_typing.EventType, - _device._EVENT_TYPE_MAPPING, - set(), - set(), - ), - ( - nvml.FanControlPolicy, - system_typing.FanControlPolicy, - _device._FAN_CONTROL_POLICY_MAPPING, - set(), - set(), - ), - ( - nvml.CoolerControl, - system_typing.CoolerControl, - _device._COOLER_CONTROL_MAPPING, - # NONE means no signal; COUNT is a sentinel - {"THERMAL_COOLER_SIGNAL_NONE", "THERMAL_COOLER_SIGNAL_COUNT"}, - set(), - ), - ( - nvml.CoolerTarget, - system_typing.CoolerTarget, - _device._COOLER_TARGET_MAPPING, - # GPU_RELATED is a composite bitmask (GPU | MEMORY | POWER_SUPPLY); - # the wrapper expands it into individual targets instead of mapping - # it as a single entry - {"THERMAL_GPU_RELATED"}, - set(), - ), - ( - nvml.ThermalController, - system_typing.ThermalController, - _device._THERMAL_CONTROLLER_MAPPING, - {"NONE"}, - {"NONE"}, - ), - ( - nvml.ThermalTarget, - system_typing.ThermalTarget, - _device._THERMAL_TARGET_MAPPING, - # UNKNOWN is a fallback sentinel; handled by .get() - {"UNKNOWN"}, - set(), - ), - ( - nvml.NvlinkVersion, - None, # maps to tuple, not a StrEnum - _device._NVLINK_VERSION_MAPPING, - # VERSION_INVALID is a sentinel for "no NvLink present" - {"VERSION_INVALID"}, - set(), - ), - ( - nvml.SystemEventType, - system_typing.SystemEventType, - _system_events._SYSTEM_EVENT_TYPE_MAPPING, - set(), - set(), - ), - ( - nvml.AffinityScope, - system_typing.AffinityScope, - _device._AFFINITY_SCOPE_MAPPING, - set(), - set(), - ), - ( - nvml.GpuP2PCapsIndex, - system_typing.GpuP2PCapsIndex, - _device._GPU_P2P_CAPS_INDEX_MAPPING, - set(), - set(), - ), - ( - nvml.GpuTopologyLevel, - system_typing.GpuTopologyLevel, - _device._GPU_TOPOLOGY_LEVEL_MAPPING, - set(), - set(), - ), - ( - nvml.ClockId, - system_typing.ClockId, - _device._CLOCK_ID_MAPPING, - # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated; COUNT is a sentinel - {"APP_CLOCK_TARGET", "APP_CLOCK_DEFAULT", "COUNT"}, - set(), - ), - ( - nvml.ClockType, - system_typing.ClockType, - _device._CLOCK_TYPE_MAPPING, - # COUNT is a sentinel - {"CLOCK_COUNT"}, - set(), - ), - ( - nvml.InforomObject, - system_typing.InforomObject, - _device._INFOROM_OBJECT_MAPPING, - # COUNT is a sentinel - {"INFOROM_COUNT"}, - set(), - ), - ( - nvml.TemperatureThresholds, - system_typing.TemperatureThresholds, - _device._TEMPERATURE_THRESHOLD_MAPPING, - # COUNT is a sentinel - {"TEMPERATURE_THRESHOLD_COUNT"}, - set(), - ), - ] +_CLOCKS_EVENT_REASONS_STR_UNMAPPED = { + core_member + for binding_member, core_member in ( + ("EVENT_REASON_BOARD_LIMIT", "BOARD_LIMIT"), + ("EVENT_REASON_RELIABILITY", "RELIABILITY"), ) + if binding_member not in nvml.ClocksEventReasons.__members__ +} + +_CASES.extend( + [ + ( + nvml.DeviceAddressingModeType, + system_typing.AddressingMode, + _device._ADDRESSING_MODE_MAPPING, + # NONE means "no special addressing mode is active"; not a valid target + {"DEVICE_ADDRESSING_MODE_NONE"}, + set(), + ), + ( + nvml.BrandType, + None, # maps to plain str, not a StrEnum + _device._BRAND_TYPE_MAPPING, + # COUNT is a sentinel, not a real brand + {"BRAND_COUNT"}, + set(), + ), + ( + nvml.GpuP2PStatus, + system_typing.GpuP2PStatus, + _device._GPU_P2P_STATUS_MAPPING, + # Both the typo'd (SUPPORED) and corrected (SUPPORTED) spellings + # share the same integer value; the mapping covers both via aliases + {"P2P_STATUS_CHIPSET_NOT_SUPPORED"}, + set(), + ), + ( + nvml.ClocksEventReasons, + system_typing.ClocksEventReasons, + _device._CLOCKS_EVENT_REASONS_MAPPING, + set(), + _CLOCKS_EVENT_REASONS_STR_UNMAPPED, + ), + ( + nvml.EventType, + system_typing.EventType, + _device._EVENT_TYPE_MAPPING, + set(), + set(), + ), + ( + nvml.FanControlPolicy, + system_typing.FanControlPolicy, + _device._FAN_CONTROL_POLICY_MAPPING, + set(), + set(), + ), + ( + nvml.CoolerControl, + system_typing.CoolerControl, + _device._COOLER_CONTROL_MAPPING, + # NONE means no signal; COUNT is a sentinel + {"THERMAL_COOLER_SIGNAL_NONE", "THERMAL_COOLER_SIGNAL_COUNT"}, + set(), + ), + ( + nvml.CoolerTarget, + system_typing.CoolerTarget, + _device._COOLER_TARGET_MAPPING, + # GPU_RELATED is a composite bitmask (GPU | MEMORY | POWER_SUPPLY); + # the wrapper expands it into individual targets instead of mapping + # it as a single entry + {"THERMAL_GPU_RELATED"}, + set(), + ), + ( + nvml.ThermalController, + system_typing.ThermalController, + _device._THERMAL_CONTROLLER_MAPPING, + {"NONE"}, + {"NONE"}, + ), + ( + nvml.ThermalTarget, + system_typing.ThermalTarget, + _device._THERMAL_TARGET_MAPPING, + # UNKNOWN is a fallback sentinel; handled by .get() + {"UNKNOWN"}, + set(), + ), + ( + nvml.NvlinkVersion, + None, # maps to tuple, not a StrEnum + _device._NVLINK_VERSION_MAPPING, + # VERSION_INVALID is a sentinel for "no NvLink present" + {"VERSION_INVALID"}, + set(), + ), + ( + nvml.SystemEventType, + system_typing.SystemEventType, + _system_events._SYSTEM_EVENT_TYPE_MAPPING, + set(), + set(), + ), + ( + nvml.AffinityScope, + system_typing.AffinityScope, + _device._AFFINITY_SCOPE_MAPPING, + set(), + set(), + ), + ( + nvml.GpuP2PCapsIndex, + system_typing.GpuP2PCapsIndex, + _device._GPU_P2P_CAPS_INDEX_MAPPING, + set(), + set(), + ), + ( + nvml.GpuTopologyLevel, + system_typing.GpuTopologyLevel, + _device._GPU_TOPOLOGY_LEVEL_MAPPING, + set(), + set(), + ), + ( + nvml.ClockId, + system_typing.ClockId, + _device._CLOCK_ID_MAPPING, + # APP_CLOCK_TARGET and APP_CLOCK_DEFAULT are deprecated; COUNT is a sentinel + {"APP_CLOCK_TARGET", "APP_CLOCK_DEFAULT", "COUNT"}, + set(), + ), + ( + nvml.ClockType, + system_typing.ClockType, + _device._CLOCK_TYPE_MAPPING, + # COUNT is a sentinel + {"CLOCK_COUNT"}, + set(), + ), + ( + nvml.InforomObject, + system_typing.InforomObject, + _device._INFOROM_OBJECT_MAPPING, + # COUNT is a sentinel + {"INFOROM_COUNT"}, + set(), + ), + ( + nvml.TemperatureThresholds, + system_typing.TemperatureThresholds, + _device._TEMPERATURE_THRESHOLD_MAPPING, + # COUNT is a sentinel + {"TEMPERATURE_THRESHOLD_COUNT"}, + set(), + ), + ] +) # StrEnum subclasses that intentionally have no associated cuda_binding. @@ -323,11 +318,10 @@ } -# CUdevWorkqueueConfigScope was added to the CUDA driver in 13.1 (missing -# from the 13.0.0 cuda.h and earlier); on cuda-bindings for CUDA 12.x or -# 13.0.x, WorkqueueSharingScopeType has no driver-side counterpart to +# CUdevWorkqueueConfigScope was added to the CUDA driver in 13.1; on the +# CUDA 12 build, WorkqueueSharingScopeType has no driver-side counterpart to # check against. -if hasattr(driver, "CUdevWorkqueueConfigScope"): +if BUILD_CUDA_MAJOR >= 13: _CASES.append( ( driver.CUdevWorkqueueConfigScope, diff --git a/cuda_core/tests/test_error_handling.py b/cuda_core/tests/test_error_handling.py index c718a0f1086..1ff3ea2b461 100644 --- a/cuda_core/tests/test_error_handling.py +++ b/cuda_core/tests/test_error_handling.py @@ -36,7 +36,7 @@ ) from cuda.core._stream import default_stream from cuda.core._utils.cuda_utils import CUDAError, driver, handle_return -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version from cuda.core.graph import GraphDefinition INVALID_CONTEXT = int(driver.CUresult.CUDA_ERROR_INVALID_CONTEXT) @@ -254,7 +254,7 @@ def test_set_current_with_context_works_without_a_current_context(init_cuda): def test_memset_update_keeps_new_owners_alive_when_context_cannot_be_restored(device_x2): """The node's new parameters stay valid: the attachment is published before the restoration failure is raised, so the updated graph instantiates and runs.""" - if driver_version() < (13, 2, 0) or binding_version() < (13, 2, 0): + if BUILD_CUDA_MAJOR < 13 or driver_version() < (13, 2, 0): pytest.skip("node contexts are only recorded by cuGraphNodeGetParams on CUDA 13.2+") node_dev, other_dev = device_x2 node_dev.set_current() diff --git a/cuda_core/tests/test_green_context.py b/cuda_core/tests/test_green_context.py index 5f1954c6b58..bf336f8e0f2 100644 --- a/cuda_core/tests/test_green_context.py +++ b/cuda_core/tests/test_green_context.py @@ -20,7 +20,7 @@ launch, ) from cuda.core._utils.cuda_utils import CUDAError, driver, handle_return -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version from cuda.core.graph import GraphDefinition from cuda.core.typing import WorkqueueSharingScopeType @@ -155,8 +155,8 @@ def test_memory_node_updates_preserve_green_context( init_cuda, green_ctx, ): - if driver_version() < (13, 2, 0) or binding_version() < (13, 2, 0): - pytest.skip("generic graph node parameter queries require CUDA 13.2+") + if BUILD_CUDA_MAJOR < 13 or driver_version() < (13, 2, 0): + pytest.skip("generic graph node parameter queries require the CUDA 13 build and driver 13.2+") memory_resource = LegacyPinnedMemoryResource() src = memory_resource.allocate(4) @@ -408,7 +408,7 @@ def test_discovery_mode(self, sm_resource): @pytest.mark.agent_authored(model="gpt-5.6-sol") def test_by_count_discovery_respects_alignment(self, sm_resource): """CUDA 12 SplitByCount discovery returns an aligned SM count.""" - if binding_version()[0] != 12: + if BUILD_CUDA_MAJOR != 12: pytest.skip("test covers the CUDA 12 SplitByCount path") groups, _ = sm_resource.split(SMResourceOptions(count=None)) diff --git a/cuda_core/tests/test_launcher.py b/cuda_core/tests/test_launcher.py index bd798424f70..d5ee2262883 100644 --- a/cuda_core/tests/test_launcher.py +++ b/cuda_core/tests/test_launcher.py @@ -1013,9 +1013,6 @@ def test_launch_graph_conditional_handle_as_kernel_arg(init_cuda, use_subclass): """CUgraphConditionalHandle is packed as its uint64 value (readback).""" from cuda.bindings import driver - if not hasattr(driver, "CUgraphConditionalHandle"): - pytest.skip("CUgraphConditionalHandle requires cuda-bindings 12.3+") - class SubclassedHandle(driver.CUgraphConditionalHandle): pass diff --git a/cuda_core/tests/test_linker.py b/cuda_core/tests/test_linker.py index c80071fa405..45444ab4b4d 100644 --- a/cuda_core/tests/test_linker.py +++ b/cuda_core/tests/test_linker.py @@ -326,12 +326,6 @@ def test_which_backend_falls_back_when_nvjitlink_too_old(self, monkeypatch): monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None) monkeypatch.setattr(_linker, "_driver", None) - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return object() - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False) with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"): @@ -350,13 +344,7 @@ def test_which_backend_falls_back_when_dylib_missing(self, monkeypatch): def raise_missing(_nvjitlink): raise DynamicLibNotFoundError("missing") - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return object() - monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing) - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): assert Linker.which_backend() == "driver" diff --git a/cuda_core/tests/test_module.py b/cuda_core/tests/test_module.py index 3e85101bd85..3b1fe9f7a9d 100644 --- a/cuda_core/tests/test_module.py +++ b/cuda_core/tests/test_module.py @@ -16,7 +16,7 @@ from cuda.core import Device, Kernel, Linker, LinkerOptions, ObjectCode, Program, ProgramOptions from cuda.core._program import _can_load_generated_ptx from cuda.core._utils.cuda_utils import CUDAError, driver, handle_return -from cuda.core._utils.version import binding_version, driver_version +from cuda.core._utils.version import driver_version try: import numba @@ -65,7 +65,7 @@ def _is_nvfatbin_available(): @pytest.fixture(scope="module") def cuda12_4_prerequisite_check(): - return binding_version() >= (12, 0, 0) and driver_version() >= (12, 4, 0) + return driver_version() >= (12, 4, 0) @pytest.fixture(name="convert_path", params=[str, lambda p: p], ids=["str", "path"]) diff --git a/cuda_core/tests/test_optional_dependency_imports.py b/cuda_core/tests/test_optional_dependency_imports.py index b08b7d344d9..68a1779a260 100644 --- a/cuda_core/tests/test_optional_dependency_imports.py +++ b/cuda_core/tests/test_optional_dependency_imports.py @@ -35,28 +35,7 @@ def restore_optional_import_state(): _linker._use_nvjitlink_backend = saved_use_nvjitlink -@pytest.mark.agent_authored(model="gpt-5.6-sol") -def test_get_nvvm_module_rejects_old_bindings(monkeypatch): - """NVVM import requires cuda-bindings >= 12.9.0 and caches a failed attempt.""" - calls = 0 - - def old_binding_version(): - nonlocal calls - calls += 1 - return (12, 8, 0) - - monkeypatch.setattr(_program, "binding_version", old_binding_version) - - with pytest.raises(RuntimeError, match="cuda-bindings >= 12.9.0"): - _program._get_nvvm_module() - with pytest.raises(RuntimeError, match="previous import attempt failed"): - _program._get_nvvm_module() - assert calls == 1 - - def test_get_nvvm_module_reraises_nested_module_not_found(monkeypatch): - monkeypatch.setattr(_program, "binding_version", lambda: (12, 9, 0)) - def fake__optional_cuda_import(modname, probe_function=None): assert modname == "cuda.bindings.nvvm" assert probe_function is not None @@ -72,8 +51,6 @@ def fake__optional_cuda_import(modname, probe_function=None): def test_get_nvvm_module_reports_missing_nvvm_module(monkeypatch): - monkeypatch.setattr(_program, "binding_version", lambda: (12, 9, 0)) - def fake__optional_cuda_import(modname, probe_function=None): assert modname == "cuda.bindings.nvvm" assert probe_function is not None @@ -86,8 +63,6 @@ def fake__optional_cuda_import(modname, probe_function=None): def test_get_nvvm_module_handles_missing_libnvvm(monkeypatch): - monkeypatch.setattr(_program, "binding_version", lambda: (12, 9, 0)) - def fake__optional_cuda_import(modname, probe_function=None): assert modname == "cuda.bindings.nvvm" assert probe_function is not None @@ -99,36 +74,6 @@ def fake__optional_cuda_import(modname, probe_function=None): _program._get_nvvm_module() -def test_decide_nvjitlink_or_driver_reraises_nested_module_not_found(monkeypatch): - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - err = ModuleNotFoundError("No module named 'not_a_real_dependency'") - err.name = "not_a_real_dependency" - raise err - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) - - with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency") as excinfo: - _linker._decide_nvjitlink_or_driver() - assert excinfo.value.name == "not_a_real_dependency" - - -def test_decide_nvjitlink_or_driver_falls_back_when_module_missing(monkeypatch): - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return None - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) - - with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): - use_driver_backend = _linker._decide_nvjitlink_or_driver() - - assert use_driver_backend is True - assert _linker._use_nvjitlink_backend is False - - @pytest.mark.agent_authored(model="grok-4.5") def test_decide_nvjitlink_or_driver_falls_back_when_dylib_missing(monkeypatch): """Missing nvJitLink dylib must fall back via DynamicLibNotFoundError.""" @@ -136,12 +81,6 @@ def test_decide_nvjitlink_or_driver_falls_back_when_dylib_missing(monkeypatch): def raise_missing(_nvjitlink): raise DynamicLibNotFoundError("libnvJitLink missing") - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return object() - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing) with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): @@ -153,12 +92,6 @@ def fake__optional_cuda_import(modname, probe_function=None): @pytest.mark.agent_authored(model="grok-4.5") def test_decide_nvjitlink_or_driver_falls_back_when_nvjitlink_too_old(monkeypatch): - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return object() - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False) with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"): @@ -170,12 +103,6 @@ def fake__optional_cuda_import(modname, probe_function=None): @pytest.mark.agent_authored(model="grok-4.5") def test_decide_nvjitlink_or_driver_selects_nvjitlink_when_version_symbol_present(monkeypatch): - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return object() - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: True) use_driver_backend = _linker._decide_nvjitlink_or_driver() @@ -189,22 +116,16 @@ def test_decide_nvjitlink_or_driver_does_not_call_version(monkeypatch): """Regression guard for #2408: must not call module.version().""" called = {"version": False, "inspect": False} - class FakeModule: - def version(self): - called["version"] = True - raise AssertionError("module.version() must not be used for nvJitLink probing") + def fake_version(): + called["version"] = True + raise AssertionError("module.version() must not be used for nvJitLink probing") def fake_has_version(_nvjitlink): called["inspect"] = True return True - def fake__optional_cuda_import(modname, probe_function=None): - assert modname == "cuda.bindings.nvjitlink" - assert probe_function is None - return FakeModule() - - monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", fake_has_version) + monkeypatch.setattr("cuda.bindings.nvjitlink.version", fake_version) assert _linker._decide_nvjitlink_or_driver() is False assert called["inspect"] is True diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index 311bbf1875f..b14c313cf20 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -60,19 +60,9 @@ def _get_nvrtc_version_for_tests(): # CUDAError from a successfully loaded library propagates (real bug). -def _has_nvrtc_pch_apis_for_tests(): - required = ( - "nvrtcGetPCHHeapSize", - "nvrtcSetPCHHeapSize", - "nvrtcGetPCHCreateStatus", - "nvrtcGetPCHHeapSizeRequired", - ) - return all(hasattr(nvrtc, name) for name in required) - - nvrtc_pch_available = pytest.mark.skipif( - (_get_nvrtc_version_for_tests() or 0) < 12800 or not _has_nvrtc_pch_apis_for_tests(), - reason="PCH runtime APIs require NVRTC >= 12.8 bindings", + (_get_nvrtc_version_for_tests() or 0) < 12800, + reason="PCH runtime APIs require NVRTC >= 12.8", ) bundled_headers_available = pytest.mark.skipif( diff --git a/cuda_core/tests/test_rt_layout.py b/cuda_core/tests/test_rt_layout.py index 53158370968..228ddfd433a 100644 --- a/cuda_core/tests/test_rt_layout.py +++ b/cuda_core/tests/test_rt_layout.py @@ -90,7 +90,7 @@ def test_umbrellas_are_named_only_by_their_cython_file(): @pytest.mark.agent_authored(model="claude-fable-5-1") def test_consumer_closure_is_types_and_the_python_seam(): closure = {p.name for p in include_closure(RT / "handles.hpp")} - assert closure == {"handles.hpp", "py.hpp", "types.hpp"} + assert closure == {"handles.hpp", "py.hpp", "types.hpp", "versions.hpp"} # Consumers are RTLD_LOCAL extensions that cannot link to _rt: nothing with storage. for name in sorted(closure): text = read(RT / name) @@ -98,6 +98,67 @@ def test_consumer_closure_is_types_and_the_python_seam(): assert not re.search(r"^(static|thread_local)\b", text, re.M), f"{name} defines storage" +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_cuda_version_is_named_only_in_versions_hpp(): + """The C++ may branch on CUDA_CORE_BUILD_MAJOR only. A `#if CUDA_VERSION >= 130x0` + fence compiled a feature out of source builds against an older header while the + run-time checks never noticed (https://github.com/NVIDIA/cuda-python/issues/2783); + versions.hpp checks the header once and is the only file allowed to name it.""" + cpp = CORE / "_cpp" + files = sorted(p for p in cpp.rglob("*") if p.suffix in (".hpp", ".h", ".cpp")) + assert len(files) > 20 + spellers = sorted(p.relative_to(cpp).as_posix() for p in files if re.search(r"\bCUDA_VERSION\b", read(p))) + assert spellers == ["rt/versions.hpp"] + assert "CUDA_CORE_BUILD_MAJOR" in read(RT / "versions.hpp") + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_driver_function_table_matches_the_cuda_bindings_loader(): + """driver_api.hpp lists each driver function with the CUDA version cuda-bindings + requests it at; that number decides which functions every supported driver + must provide. Check it against the loader cuda-bindings generates.""" + loader = CORE.parents[2] / "cuda_bindings" / "cuda" / "bindings" / "_internal" / "driver_linux.pyx" + if not loader.is_file(): + pytest.skip("cuda-bindings source is not next to cuda_core") + entries = re.findall(r"^\s*X\((cu\w+), (\d+)\)", read(RT / "driver_api.hpp"), re.M) + assert len(entries) >= 60 + assert len({name for name, _ in entries}) == len(entries), "duplicate table entry" + requested = {} + for name, version in re.findall(r"cuGetProcAddress_v2\('(\w+)', &__\w+, (\d+)", read(loader)): + requested.setdefault(name, set()).add(int(version)) + mismatched = { + name: (int(introduced), sorted(requested.get(name, ()))) + for name, introduced in entries + if int(introduced) not in requested.get(name, ()) + } + assert mismatched == {} + + +@pytest.mark.agent_authored(model="claude-fable-5-1") +def test_driver_calls_go_through_the_table(): + """Every driver call uses DRIVER_CALL (or a pw_ wrapper), which resolves the + table on first use and never dereferences null. The only raw p_ calls are + the table's own machinery and the sites under ipc_import_mutex, where the + table is resolved before the lock and marked `// raw:`.""" + machinery = {"driver_api.hpp", "driver_api.cpp", "py_driver_fns.cpp", "internal.hpp"} + raw_call = re.compile(r"\bp_(cu|nv)\w+\(") + offenders = [] + for path in HEADERS + SOURCES: + if path.name in machinery: + continue + for number, line in enumerate(read(path).splitlines(), 1): + if raw_call.search(line) and "// raw:" not in line and not line.lstrip().startswith("//"): + offenders.append(f"{path.name}:{number}") + assert offenders == [] + marked = [ + f"{path.name}:{number}" + for path in SOURCES + for number, line in enumerate(read(path).splitlines(), 1) + if "// raw:" in line + ] + assert {name.split(":")[0] for name in marked} == {"memory.cpp"}, marked + + @pytest.mark.agent_authored(model="claude-fable-5-1") def test_pxd_functions_are_not_called_by_name_inside_the_module(): """Cython emits a static prototype for each cdef function the .pxd declares, so diff --git a/cuda_core/tests/test_utils_enum_explanations_helpers.py b/cuda_core/tests/test_utils_enum_explanations_helpers.py index 6d4c9e32b82..0cbb2672de0 100644 --- a/cuda_core/tests/test_utils_enum_explanations_helpers.py +++ b/cuda_core/tests/test_utils_enum_explanations_helpers.py @@ -2,15 +2,10 @@ # # SPDX-License-Identifier: Apache-2.0 -import importlib -import sys - import pytest -from cuda.core._utils import enum_explanations_helpers from cuda.core._utils.enum_explanations_helpers import ( DocstringBackedExplanations, - _binding_version_has_usable_enum_docstrings, clean_enum_member_docstring, ) @@ -90,81 +85,3 @@ def test_docstring_backed_get_returns_default_for_missing_docstring(): lut = DocstringBackedExplanations(_FakeEnumType({7: _FakeEnumMember(None)})) assert lut.get(7) is None assert lut.get(7, default="sentinel") == "sentinel" - - -@pytest.mark.parametrize( - ("version", "expected"), - [ - pytest.param((12, 9, 5), False, id="before_12_9_6"), - pytest.param((12, 9, 6), True, id="from_12_9_6"), - pytest.param((13, 0, 0), False, id="13_0_mainline_gap"), - pytest.param((13, 1, 1), False, id="13_1_1"), - pytest.param((13, 2, 0), True, id="from_13_2_0"), - ], -) -def test_binding_version_has_usable_enum_docstrings(version, expected): - assert _binding_version_has_usable_enum_docstrings(version) is expected - - -@pytest.mark.parametrize( - ("version", "expects_docstrings"), - [ - pytest.param((12, 9, 5), False, id="before_12_9_6"), - pytest.param((12, 9, 6), True, id="from_12_9_6"), - pytest.param((13, 0, 0), False, id="13_0_mainline_gap"), - pytest.param((13, 2, 0), True, id="from_13_2_0"), - ], -) -def test_get_best_available_explanations_switches_by_version(monkeypatch, version, expects_docstrings): - fallback = {7: "fallback text"} - monkeypatch.setattr(enum_explanations_helpers, "_binding_version", lambda: version) - expl = enum_explanations_helpers.get_best_available_explanations( - _FakeEnumType({7: _FakeEnumMember("clean me")}), - fallback, - ) - if expects_docstrings: - assert isinstance(expl, DocstringBackedExplanations) - assert expl.get(7) == "clean me" - else: - assert expl is fallback - - -def test_get_best_available_explanations_calls_loader_before_docstrings(monkeypatch): - fallback = {7: "fallback text"} - calls = [] - - def load_fallback(): - calls.append("loaded") - return fallback - - monkeypatch.setattr(enum_explanations_helpers, "_binding_version", lambda: (13, 1, 1)) - expl = enum_explanations_helpers.get_best_available_explanations( - _FakeEnumType({7: _FakeEnumMember("clean me")}), - load_fallback, - ) - assert expl is fallback - assert calls == ["loaded"] - - -def test_driver_explanations_module_skips_fallback_import_when_docstrings_available(monkeypatch): - import cuda.core._utils.driver_cu_result_explanations as driver_explanations - - monkeypatch.setattr(enum_explanations_helpers, "_binding_version", lambda: (13, 2, 0)) - sys.modules.pop("cuda.core._utils.driver_cu_result_explanations_frozen", None) - - importlib.reload(driver_explanations) - - assert "cuda.core._utils.driver_cu_result_explanations_frozen" not in sys.modules - assert isinstance(driver_explanations.DRIVER_CU_RESULT_EXPLANATIONS, DocstringBackedExplanations) - - -def test_runtime_explanations_module_skips_fallback_import_when_docstrings_available(monkeypatch): - import cuda.core._utils.runtime_cuda_error_explanations as runtime_explanations - - monkeypatch.setattr(enum_explanations_helpers, "_binding_version", lambda: (13, 2, 0)) - sys.modules.pop("cuda.core._utils.runtime_cuda_error_explanations_frozen", None) - - importlib.reload(runtime_explanations) - - assert "cuda.core._utils.runtime_cuda_error_explanations_frozen" not in sys.modules - assert isinstance(runtime_explanations.RUNTIME_CUDA_ERROR_EXPLANATIONS, DocstringBackedExplanations)