Coverage for cuda / pathfinder / _dynamic_libs / load_nvidia_dynamic_lib.py: 97.87%

94 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-29 01:27 +0000

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

2# SPDX-License-Identifier: Apache-2.0 

3 

4from __future__ import annotations 

5 

6import functools 

7import struct 

8import subprocess 

9import sys 

10from typing import TYPE_CHECKING 

11 

12from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS 

13from cuda.pathfinder._dynamic_libs.load_dl_common import ( 

14 DynamicLibNotAvailableError, 

15 DynamicLibNotFoundError, 

16 DynamicLibUnknownError, 

17 LoadedDL, 

18 load_dependencies, 

19) 

20from cuda.pathfinder._dynamic_libs.platform_loader import LOADER 

21from cuda.pathfinder._dynamic_libs.search_steps import ( 

22 EARLY_FIND_STEPS, 

23 LATE_FIND_STEPS, 

24 SearchContext, 

25 derive_ctk_root, 

26 find_via_ctk_root, 

27 run_find_steps, 

28) 

29from cuda.pathfinder._dynamic_libs.subprocess_protocol import ( 

30 DYNAMIC_LIB_SUBPROCESS_CWD, 

31 MODE_CANARY, 

32 STATUS_OK, 

33 DynamicLibSubprocessPayload, 

34 build_dynamic_lib_subprocess_command, 

35 parse_dynamic_lib_subprocess_payload, 

36) 

37from cuda.pathfinder._utils.platform_aware import IS_WINDOWS 

38 

39if TYPE_CHECKING: 

40 from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor 

41 

42# All libnames recognized by load_nvidia_dynamic_lib, across all categories 

43# (CTK, third-party, driver). 

44_ALL_KNOWN_LIBNAMES: frozenset[str] = frozenset(LIB_DESCRIPTORS) 

45_ALL_SUPPORTED_LIBNAMES: frozenset[str] = frozenset( 

46 name for name, desc in LIB_DESCRIPTORS.items() if (desc.windows_dlls if IS_WINDOWS else desc.linux_sonames) 

47) 

48_PLATFORM_NAME = "Windows" if IS_WINDOWS else "Linux" 

49_CANARY_PROBE_TIMEOUT_SECONDS = 10.0 

50 

51# Driver libraries: shipped with the NVIDIA display driver, always on the 

52# system linker path. These skip all CTK search steps (site-packages, 

53# conda, CUDA_PATH, canary) and go straight to system search. 

54_DRIVER_ONLY_LIBNAMES = frozenset(name for name, desc in LIB_DESCRIPTORS.items() if desc.packaged_with == "driver") 

55 

56 

57def _load_driver_lib_no_cache(desc: LibDescriptor) -> LoadedDL: 

58 """Load an NVIDIA driver library (system-search only). 

59 

60 Driver libs (libcuda, libnvidia-ml) are part of the display driver, not 

61 the CUDA Toolkit. They are expected to be discoverable via the platform's 

62 native loader mechanisms, so the full CTK search cascade (site-packages, 

63 conda, CUDA_PATH, canary) is unnecessary. 

64 """ 

65 loaded = LOADER.check_if_already_loaded_from_elsewhere(desc, False) 1azABJf

66 if loaded is not None: 1azABJf

67 return loaded 1J

68 loaded = LOADER.load_with_system_search(desc) 1azABf

69 if loaded is not None: 1azABf

70 return loaded 1azAf

71 raise DynamicLibNotFoundError( 1B

72 f'"{desc.name}" is an NVIDIA driver library and can only be found via' 

73 f" system search. Ensure the NVIDIA display driver is installed." 

74 ) 

75 

76 

77def _coerce_subprocess_output(output: str | bytes | None) -> str: 

78 if isinstance(output, bytes): 1ij

79 return output.decode(errors="replace") 

80 return "" if output is None else output 1ij

81 

82 

83def _raise_canary_probe_child_process_error( 

84 *, 

85 returncode: int | None = None, 

86 timeout: float | None = None, 

87 stderr: str | bytes | None = None, 

88) -> None: 

89 if timeout is None: 1ij

90 error_line = f"Canary probe child process exited with code {returncode}." 1i

91 else: 

92 error_line = f"Canary probe child process timed out after {timeout} seconds." 1j

93 raise ChildProcessError( 1ij

94 f"{error_line}\n" 

95 "--- stderr-from-child-process ---\n" 

96 f"{_coerce_subprocess_output(stderr)}" 

97 "<end-of-stderr-from-child-process>\n" 

98 ) 

99 

100 

101@functools.cache 

102def _resolve_system_loaded_abs_path_in_subprocess( 

103 libname: str, 

104 *, 

105 timeout: float = _CANARY_PROBE_TIMEOUT_SECONDS, 

106) -> str | None: 

107 """Resolve a canary library's absolute path in a fresh Python subprocess.""" 

108 try: 1iFGjHqrmstnuvwx

109 result = subprocess.run( # noqa: S603 - trusted argv: current interpreter + internal probe module 1iFGjHqrmstnuvwx

110 build_dynamic_lib_subprocess_command(MODE_CANARY, libname), 

111 capture_output=True, 

112 text=True, 

113 timeout=timeout, 

114 check=False, 

115 cwd=DYNAMIC_LIB_SUBPROCESS_CWD, 

116 ) 

117 except subprocess.TimeoutExpired as exc: 1j

118 _raise_canary_probe_child_process_error(timeout=exc.timeout, stderr=exc.stderr) 1j

119 

120 if result.returncode != 0: 1iFGHqrmstnuvwx

121 _raise_canary_probe_child_process_error(returncode=result.returncode, stderr=result.stderr) 1i

122 

123 payload: DynamicLibSubprocessPayload = parse_dynamic_lib_subprocess_payload( 1FGHqrmstnuvwx

124 result.stdout, 

125 libname=libname, 

126 error_label="Canary probe child process", 

127 ) 

128 abs_path: str | None = payload.abs_path 1qrmstnuvwx

129 if payload.status == STATUS_OK: 1qrmstnuvwx

130 return abs_path 1qtnvx

131 return None 1rmsnuw

132 

133 

134def _loadable_via_canary_subprocess(libname: str, *, timeout: float = _CANARY_PROBE_TIMEOUT_SECONDS) -> bool: 

135 """Return True if the canary subprocess can resolve ``libname`` via system search.""" 

136 return _resolve_system_loaded_abs_path_in_subprocess(libname, timeout=timeout) is not None 1mOP

137 

138 

139def _try_ctk_root_canary(ctx: SearchContext) -> str | None: 

140 """Try CTK-root canary fallback for descriptor-configured libraries.""" 

141 for canary_libname in ctx.desc.ctk_root_canary_anchor_libnames: 1obypCDc

142 canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname) 1obypCDc

143 if canary_abs_path is None: 1obypCDc

144 continue 1CDc

145 ctk_root = derive_ctk_root(canary_abs_path) 1obyp

146 if ctk_root is None: 1obyp

147 continue 1y

148 find = find_via_ctk_root(ctx, ctk_root) 1obp

149 if find is not None: 1obp

150 return str(find.abs_path) 1ob

151 return None 1ypCDc

152 

153 

154def _load_lib_no_cache(libname: str) -> LoadedDL: 

155 desc = LIB_DESCRIPTORS[libname] 1abgedKLMhfkcl

156 

157 if libname in _DRIVER_ONLY_LIBNAMES: 1abgedKLMhfkcl

158 return _load_driver_lib_no_cache(desc) 1aKLMf

159 

160 ctx = SearchContext(desc) 1abgedhkcl

161 

162 # Phase 1: Try to find the library file on disk (pip wheels, conda). 

163 find = run_find_steps(ctx, EARLY_FIND_STEPS) 1abgedhkcl

164 

165 # Phase 2: Cross-cutting — already-loaded check and dependency loading. 

166 # The already-loaded check on Windows uses the "have we found a path?" 

167 # flag to decide whether to apply AddDllDirectory side-effects. 

168 loaded = LOADER.check_if_already_loaded_from_elsewhere(desc, find is not None) 1abgedhkcl

169 load_dependencies(desc, load_nvidia_dynamic_lib) 1abgedhkcl

170 if loaded is not None: 1abgedhkcl

171 return loaded 

172 

173 # Phase 3: Load from found path, or fall back to system search + late find. 

174 if find is not None: 1abgedhkcl

175 return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) 1kl

176 

177 loaded = LOADER.load_with_system_search(desc) 1abgedhc

178 if loaded is not None: 1abgedhc

179 return loaded 1adh

180 

181 find = run_find_steps(ctx, LATE_FIND_STEPS) 1abgec

182 if find is not None: 1abgec

183 return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) 1ag

184 

185 if desc.ctk_root_canary_anchor_libnames: 1bec

186 canary_abs_path = _try_ctk_root_canary(ctx) 1bc

187 if canary_abs_path is not None: 1bc

188 return LOADER.load_with_abs_path(desc, canary_abs_path, "system-ctk-root") 1b

189 

190 ctx.raise_not_found() 1ec

191 

192 

193@functools.cache 

194def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: 

195 """Load an NVIDIA dynamic library by name. 

196 

197 Args: 

198 libname (str): The short name of the library to load (e.g., ``"cudart"``, 

199 ``"nvvm"``, etc.). 

200 

201 Returns: 

202 LoadedDL: Object containing the OS library handle and absolute path. 

203 

204 **Important:** 

205 

206 **Never close the returned handle.** Do **not** call ``dlclose`` (Linux) or 

207 ``FreeLibrary`` (Windows) on the ``LoadedDL._handle_uint``. 

208 

209 **Why:** the return value is cached (``functools.cache``) and shared across the 

210 process. Closing the handle can unload the module while other code still uses 

211 it, leading to crashes or subtle failures. 

212 

213 This applies to Linux and Windows. For context, see issue #1011: 

214 https://github.com/NVIDIA/cuda-python/issues/1011 

215 

216 Raises: 

217 DynamicLibUnknownError: If ``libname`` is not a recognized library name. 

218 DynamicLibNotAvailableError: If ``libname`` is recognized but not 

219 supported on this platform. 

220 DynamicLibNotFoundError: If the library cannot be found or loaded. 

221 RuntimeError: If Python is not 64-bit. 

222 

223 Search order: 

224 0. **Already loaded in the current process** 

225 

226 - If a matching library is already loaded by some other component, 

227 return its absolute path and handle and skip the rest of the search. 

228 

229 1. **NVIDIA Python wheels** 

230 

231 - Scan installed distributions (``site-packages``) to find libraries 

232 shipped in NVIDIA wheels. 

233 

234 2. **Conda environment** 

235 

236 - Conda installations are discovered via ``CONDA_PREFIX``, which is 

237 defined automatically in activated conda environments (see 

238 https://docs.conda.io/projects/conda-build/en/stable/user-guide/environment-variables.html). 

239 

240 3. **OS default mechanisms** 

241 

242 - Fall back to the native loader: 

243 

244 - Linux: ``dlopen()`` 

245 

246 - Windows: ``LoadLibraryExW()`` 

247 

248 On Linux, CUDA Toolkit (CTK) system installs with system config updates are 

249 usually discovered via ``/etc/ld.so.conf.d/*cuda*.conf``. 

250 

251 On Windows, under Python 3.8+, CPython configures the process with 

252 ``SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)``. 

253 As a result, the native DLL search used here does **not** include 

254 the system ``PATH``. 

255 

256 4. **Environment variables** 

257 

258 - If set, use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order). 

259 On Windows, this is the typical way system-installed CTK DLLs are 

260 located. Note that the NVIDIA CTK installer automatically 

261 adds ``CUDA_PATH`` to the system-wide environment. 

262 

263 5. **CTK root canary probe (discoverable libs only)** 

264 

265 - For selected libraries whose shared object doesn't reside on the 

266 standard linker path (currently ``nvvm``), attempt to derive CTK 

267 root by system-loading a well-known CTK canary library in a 

268 subprocess and then searching relative to that root. On Windows, 

269 the canary uses the same native ``LoadLibraryExW`` semantics as 

270 step 3, so there is also no ``PATH``-based discovery. 

271 

272 **Driver libraries** (``"cuda"``, ``"nvml"``): 

273 

274 These are part of the NVIDIA display driver (not the CUDA Toolkit) and 

275 are expected to be reachable via the native OS loader path. For these 

276 libraries the search is simplified to: 

277 

278 0. Already loaded in the current process 

279 1. OS default mechanisms (``dlopen`` / ``LoadLibraryExW``) 

280 

281 The CTK-specific steps (site-packages, conda, ``CUDA_PATH``, canary 

282 probe) are skipped entirely. 

283 

284 Notes: 

285 The search is performed **per library**. There is currently no mechanism to 

286 guarantee that multiple libraries are all resolved from the same location. 

287 

288 """ 

289 pointer_size_bits = struct.calcsize("P") * 8 1adfENI

290 if pointer_size_bits != 64: 1adfENI

291 raise RuntimeError( 1N

292 f"cuda.pathfinder.load_nvidia_dynamic_lib() requires 64-bit Python." 

293 f" Currently running: {pointer_size_bits}-bit Python" 

294 f" {sys.version_info.major}.{sys.version_info.minor}" 

295 ) 

296 if libname not in _ALL_KNOWN_LIBNAMES: 1adfEI

297 raise DynamicLibUnknownError(f"Unknown library name: {libname!r}. Known names: {sorted(_ALL_KNOWN_LIBNAMES)}") 1I

298 if libname not in _ALL_SUPPORTED_LIBNAMES: 1adfE

299 raise DynamicLibNotAvailableError( 1E

300 f"Library name {libname!r} is known but not available on {_PLATFORM_NAME}. " 

301 f"Supported names on {_PLATFORM_NAME}: {sorted(_ALL_SUPPORTED_LIBNAMES)}" 

302 ) 

303 return _load_lib_no_cache(libname) 1adf