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

1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5"""The cuda-bindings version floor: how it is read and how it is enforced. 

6 

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. 

13 

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`: 

17 

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. 

28 

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""" 

32 

33from __future__ import annotations 

34 

35import re 

36from collections.abc import Mapping, Sequence 

37 

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] 

47 

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+)\.\*$") 

60 

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" 

63 

64 

65def release_triple(version: str) -> tuple[int, int, int] | None: 

66 """The leading ``major.minor.patch`` of a version string, or None. 

67 

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

77 

78 

79def format_version(triple: tuple[int, ...]) -> str: 

80 return ".".join(str(part) for part in triple) 1sjLKB

81 

82 

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

86 

87 

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``. 

90 

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

128 

129 

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

137 

138 

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. 

141 

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

147 

148 

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

152 

153 

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. 

163 

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. 

169 

170 Raises ImportError with an actionable message unless the installed 

171 cuda-bindings passes all of these checks: 

172 

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. 

180 

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