Coverage for cuda/core/_bindings_floor.py: 72.46%
69 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 01:23 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 01:23 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2#
3# SPDX-License-Identifier: Apache-2.0
5"""The cuda-bindings version floor: how it is read and how it is enforced.
7cuda.core supports two CUDA major series at a time. For each series, it
8requires a minimum cuda-bindings version, the *floor*, at build time and at
9run time. The floor is the newest cuda-bindings release of that series at the
10time of the cuda.core release. Normally that is the release that the cuda.core
11wheels are built against. See https://github.com/NVIDIA/cuda-python/issues/2783
12and the support policy in the documentation.
14Exactly one place declares the floors: the ``cu12`` and ``cu13`` extras in
15``pyproject.toml``. Each extra pins ``cuda-bindings>=<floor>,<<next major>``.
16Everything else derives from them through :func:`floors_from_extras`:
18- The build backend, ``build_hooks.py``, checks the installed cuda-bindings
19 against the floor. It checks that the ``cuda.h`` that it compiles against is
20 the one that cuda-bindings was generated from. It passes the floor to the
21 ``_build_info`` extension module, which records it next to the header the
22 compiler used.
23- ``cuda/core/__init__.py`` checks the installed cuda-bindings against that
24 record with :func:`check_installed_bindings`.
25- The documentation reads the floors into substitutions in ``docs/source/conf.py``.
26- ``toolshed/check_cuda_core_bindings_floor.py``, a pre-commit hook, checks
27 that the CI toolkit pins in ``ci/versions.yml`` sit in the minors of the floors.
29The build and the tools above load this module by file path. It therefore uses
30the standard library only and must not import anything from ``cuda``.
31"""
33from __future__ import annotations
35import re
36from collections.abc import Mapping, Sequence
38__all__ = [
39 "SUPPORT_URL",
40 "bindings_requirement",
41 "check_installed_bindings",
42 "cuda_version_of",
43 "floors_from_extras",
44 "format_version",
45 "release_triple",
46]
48_RELEASE_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
49_EXTRA_RE = re.compile(r"^cu(\d+)$")
50# A cuda-bindings requirement, with or without extras such as `[all]`, up to
51# an optional environment marker. After the name, the lookahead requires `[`, an
52# operator, a marker, whitespace or the end, so that other names do not match.
53_REQUIREMENT_RE = re.compile(
54 r"^\s*cuda-bindings(?=[\[<>=!~;\s]|$)\s*(?:\[[^\]]*\])?\s*(?P<specifiers>[^;]*?)\s*(?:;.*)?$"
55)
56_FLOOR_SPEC_RE = re.compile(r"^>=(\d+)\.(\d+)\.(\d+)$")
57# The upper bound: `<14`, `<14.0` or `<14.0.0`, or the equivalent `==13.*`.
58_UPPER_SPEC_RE = re.compile(r"^<(\d+)(?:\.0)*$")
59_MAJOR_SPEC_RE = re.compile(r"^==(\d+)\.\*$")
61# The support policy section the error messages refer to.
62SUPPORT_URL = "https://nvidia.github.io/cuda-python/cuda-core/latest/support.html#cuda-core-bindings-floor"
65def release_triple(version: str) -> tuple[int, int, int] | None:
66 """The leading ``major.minor.patch`` of a version string, or None.
68 The function ignores pre-release and development suffixes, so ``13.4.1``,
69 ``13.4.1a0`` and ``13.4.2.dev249+gabcdef`` yield (13, 4, 1), (13, 4, 1)
70 and (13, 4, 2). A string without three leading integers yields None, for
71 example the ``0.1.dev1`` that setuptools-scm reports for a shallow clone.
72 """
73 m = _RELEASE_RE.match(version.strip()) 1opqrtumvnsyzjCDEFGHIJ
74 if m is None: 1opqrtumvnsyzjCDEFGHIJ
75 return None 1yzCEJ
76 return int(m.group(1)), int(m.group(2)), int(m.group(3)) 1opqrtumvnsjDFGHI
79def format_version(triple: tuple[int, ...]) -> str:
80 return ".".join(str(part) for part in triple) 1sjLKB
83def cuda_version_of(triple: tuple[int, int, int]) -> int:
84 """The ``CUDA_VERSION`` macro value for the major.minor of a version triple, for example 13040."""
85 return triple[0] * 1000 + triple[1] * 10 1MopqrtuvnsyzjaB
88def floors_from_extras(extras: Mapping[str, Sequence[str]]) -> dict[int, tuple[int, int, int]]:
89 """The floor per CUDA major, from the ``cu<major>`` extras of ``pyproject.toml``.
91 ``extras`` is the parsed ``[project.optional-dependencies]`` table. Each
92 ``cu<N>`` extra must list exactly one cuda-bindings requirement with two
93 specifiers, in either order. One is the floor, ``>=<N>.<minor>.<patch>``.
94 The other is an upper bound that excludes the next major, ``<<N+1>`` or
95 ``==<N>.*``. Anything else raises ValueError with the name of the extra, so
96 a typo fails the build and the pre-commit hook instead of a silent shift of
97 the floor.
98 """
99 floors: dict[int, tuple[int, int, int]] = {} 1abcwxkihldefgA
100 for extra, requirements in extras.items(): 1abcwxkihldefgA
101 m = _EXTRA_RE.match(extra) 1abcwxkihldefgA
102 if m is None: 1abcwxkihldefgA
103 continue 1bA
104 major = int(m.group(1)) 1abcwxkihldefg
105 matches = [(r, rm) for r in requirements if (rm := _REQUIREMENT_RE.match(r)) is not None] 1abcwxkihldefg
106 if len(matches) != 1: 1abcwxkihldefg
107 raise ValueError( 1wx
108 f"the {extra!r} extra must list exactly one cuda-bindings requirement, found {len(matches)}"
109 )
110 requirement, requirement_match = matches[0] 1abckihldefg
111 specifiers = [s.strip() for s in requirement_match.group("specifiers").split(",") if s.strip()] 1abckihldefg
112 floor_matches = [fm for s in specifiers if (fm := _FLOOR_SPEC_RE.match(s)) is not None] 1abckihldefg
113 confined = [cm for s in specifiers if (cm := _confined_major(s)) is not None] 1abckihldefg
114 if len(specifiers) != 2 or len(floor_matches) != 1 or len(confined) != 1: 1abckihldefg
115 raise ValueError( 1kihl
116 f"the {extra!r} extra must pin cuda-bindings as '>=<floor>,<<next major>', "
117 f"for example '>=13.4.1,<14', but it lists {requirement!r}"
118 )
119 floor = (int(floor_matches[0].group(1)), int(floor_matches[0].group(2)), int(floor_matches[0].group(3))) 1abcdefg
120 if floor[0] != major or confined[0] != major: 1abcdefg
121 raise ValueError( 1defg
122 f"the {extra!r} extra pins cuda-bindings {requirement!r}, which does not confine it to CUDA {major}"
123 )
124 floors[major] = floor 1abc
125 if not floors: 1abcA
126 raise ValueError("pyproject.toml declares no cu<major> extra") 1A
127 return dict(sorted(floors.items())) 1abc
130def _confined_major(specifier: str) -> int | None:
131 """The one major a specifier confines cuda-bindings to: ``<14`` -> 13, ``==13.*`` -> 13, else None."""
132 if (m := _UPPER_SPEC_RE.match(specifier)) is not None: 1abckihldefg
133 return int(m.group(1)) - 1 1abcihdef
134 if (m := _MAJOR_SPEC_RE.match(specifier)) is not None: 1abckihldefg
135 return int(m.group(1)) 1bhg
136 return None 1abckihldefg
139def bindings_requirement(floor: tuple[int, int, int], below: tuple[int, ...] | None = None) -> str:
140 """The pip requirement for cuda-bindings at or above ``floor`` and below ``below``, without extras.
142 ``below`` defaults to the next major: ``(13, 4, 1)`` gives ``cuda-bindings>=13.4.1,<14``.
143 With ``below=(13, 5)`` it gives ``cuda-bindings>=13.4.1,<13.5``.
144 """
145 upper = format_version(below) if below is not None else str(floor[0] + 1) 1B
146 return f"cuda-bindings>={format_version(floor)},<{upper}" 1B
149def header_minor(cuda_version: int) -> tuple[int, int]:
150 """The (major, minor) of a ``CUDA_VERSION`` macro value: 13040 -> (13, 4)."""
151 return cuda_version // 1000, cuda_version // 10 % 100 1opqrmnjKaN
154def check_installed_bindings(
155 installed_version: str,
156 installed_cuda_version: int,
157 build_cuda_major: int,
158 build_cuda_version: int,
159 build_floor: tuple[int, int, int],
160 core_version: str,
161) -> tuple[int, int, int]:
162 """Validate the installed cuda-bindings against a build and return its triple.
164 ``installed_version`` and ``installed_cuda_version`` are the ``__version__``
165 and ``driver.CUDA_VERSION`` of the installed cuda-bindings. The latter is
166 the ``cuda.h`` that it was generated from, for example 13040.
167 ``build_cuda_major``, ``build_cuda_version`` and ``build_floor`` are the
168 build's record in the ``_build_info`` extension module.
170 Raises ImportError with an actionable message unless the installed
171 cuda-bindings passes all of these checks:
173 - It is a release.
174 - It is of the major that this build was compiled for.
175 - It is at least the floor.
176 - It was generated from a ``cuda.h`` minor at least as new as the one that
177 the build compiled against. The driver function table that the C++ layer
178 looks up in cuda-bindings is keyed by the macros of that header, so an
179 older minor may lack entries.
181 The check compares headers as ``CUDA_VERSION`` values, not version strings,
182 because a development build of cuda-bindings carries the previous release's
183 version string.
184 """
185 installed = release_triple(installed_version) 1opqrtumvnsyzj
186 if installed is None: 1opqrtumvnsyzj
187 raise ImportError( 1yz
188 f"cuda.core requires a cuda-bindings {build_cuda_major}.x release, "
189 f"but the installed cuda-bindings version is {installed_version}"
190 )
191 major = installed[0] 1opqrtumvnsj
192 if major != build_cuda_major: 1opqrtumvnsj
193 raise ImportError( 1tuv
194 f"This cuda.core {core_version} build is for CUDA {build_cuda_major}, but the installed "
195 f"cuda-bindings is {installed_version}. Install cuda-bindings {build_cuda_major}.x with: "
196 f'pip install "cuda-bindings=={build_cuda_major}.*". If a cuda.core build for CUDA {major} exists, '
197 "install it instead."
198 )
199 if installed < tuple(build_floor): 1opqrmnsj
200 floor = format_version(build_floor) 1sj
201 raise ImportError( 1sj
202 f"cuda.core {core_version} requires cuda-bindings >= {floor} for CUDA {major}, but cuda-bindings "
203 f'{installed_version} is installed. Upgrade with: pip install -U "cuda-bindings>={floor},<{major + 1}"'
204 )
205 built_against, generated_from = header_minor(build_cuda_version), header_minor(installed_cuda_version) 1opqrmnj
206 if generated_from < built_against: 1opqrmnj
207 needed = f"{built_against[0]}.{built_against[1]}" 1mn
208 raise ImportError( 1mn
209 f"cuda.core {core_version} was compiled against CUDA {needed} headers and needs cuda-bindings "
210 f"{needed} or newer, but cuda-bindings {installed_version} is installed. This does not require a "
211 f"newer CUDA driver or toolkit: cuda.core supports older CUDA {major}.x versions at run time, "
212 f'see {SUPPORT_URL}. Upgrade with: pip install -U "cuda-bindings>={needed}.0,<{major + 1}"'
213 )
214 return installed 1opqrmj