Coverage for cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py: 97.94%
97 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-29 01:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-29 01:38 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6import functools
7import struct
8import subprocess
9import sys
10from typing import TYPE_CHECKING
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
39if TYPE_CHECKING:
40 from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
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
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")
57def _load_driver_lib_no_cache(desc: LibDescriptor) -> LoadedDL:
58 """Load an NVIDIA driver library (system-search only).
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) 1aDEF1f
66 if loaded is not None: 1aDEF1f
67 return loaded 1a1
68 loaded = LOADER.load_with_system_search(desc) 1aDEFf
69 if loaded is not None: 1aDEFf
70 return loaded 1aDEf
71 raise DynamicLibNotFoundError( 1F
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 )
77def _coerce_subprocess_output(output: str | bytes | None) -> str:
78 if isinstance(output, bytes): 1mn
79 return output.decode(errors="replace")
80 return "" if output is None else output 1mn
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: 1mn
90 error_line = f"Canary probe child process exited with code {returncode}." 1m
91 else:
92 error_line = f"Canary probe child process timed out after {timeout} seconds." 1n
93 raise ChildProcessError( 1mn
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 )
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: 1mRSnTwxrhiyzABsCt
109 result = subprocess.run( # noqa: S603 - trusted argv: current interpreter + internal probe module 1mRSnTwxrhiyzABsCt
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: 1n
118 _raise_canary_probe_child_process_error(timeout=exc.timeout, stderr=exc.stderr) 1n
120 if result.returncode != 0: 1mRSTwxrhiyzABsCt
121 _raise_canary_probe_child_process_error(returncode=result.returncode, stderr=result.stderr) 1m
123 payload: DynamicLibSubprocessPayload = parse_dynamic_lib_subprocess_payload( 1RSTwxrhiyzABsCt
124 result.stdout,
125 libname=libname,
126 error_label="Canary probe child process",
127 )
128 abs_path: str | None = payload.abs_path 1wxrhiyzABsCt
129 if payload.status == STATUS_OK: 1wxrhiyzABsCt
130 return abs_path 1wizAs
131 return None 1xrhyBsCt
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 1rt7
139def resolve_ctk_root_via_canary(canary_libname: str) -> str | None:
140 """Resolve the CUDA Toolkit root from a system-loadable canary library.
142 The canary library's absolute path is resolved by the OS dynamic loader in
143 an isolated subprocess, which honors ``LD_LIBRARY_PATH`` on Linux and the
144 native DLL search on Windows. The toolkit root is then derived from that
145 path. Returns ``None`` if the canary cannot be resolved or no root can be
146 derived.
147 """
148 canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname) 1kboluv2UVGWHhIJXKLiMYNZOPc
149 if canary_abs_path is None: 1kboluv2UVGWHhIJXKLiMYNZOPc
150 return None 1uv2GHhIJKLiMNOPc
151 ctk_root: str | None = derive_ctk_root(canary_abs_path) 1kbolUVGWHhIJXKLiMYNZOP
152 return ctk_root 1kbolUVGWHhIJXKLiMYNZOP
155def _try_ctk_root_canary(ctx: SearchContext) -> str | None:
156 """Try CTK-root canary fallback for descriptor-configured libraries."""
157 for canary_libname in ctx.desc.ctk_root_canary_anchor_libnames: 1kboluvc
158 ctk_root = resolve_ctk_root_via_canary(canary_libname) 1kboluvc
159 if ctk_root is None: 1kboluvc
160 continue 1ouvc
161 find = find_via_ctk_root(ctx, ctk_root) 1kbl
162 if find is not None: 1kbl
163 return str(find.abs_path) 1kb
164 return None 1oluvc
167def _load_lib_no_cache(libname: str) -> LoadedDL:
168 desc = LIB_DESCRIPTORS[libname] 1abged345jfpcq
170 if libname in _DRIVER_ONLY_LIBNAMES: 1abged345jfpcq
171 return _load_driver_lib_no_cache(desc) 1a345f
173 ctx = SearchContext(desc) 1abgedjpcq
175 # Phase 1: Try to find the library file on disk (pip wheels, conda).
176 find = run_find_steps(ctx, EARLY_FIND_STEPS) 1abgedjpcq
178 # Phase 2: Cross-cutting — already-loaded check and dependency loading.
179 # The already-loaded check on Windows uses the "have we found a path?"
180 # flag to decide whether to apply AddDllDirectory side-effects.
181 loaded = LOADER.check_if_already_loaded_from_elsewhere(desc, find is not None) 1abgedjpcq
182 load_dependencies(desc, load_nvidia_dynamic_lib) 1abgedjpcq
183 if loaded is not None: 1abgedjpcq
184 return loaded
186 # Phase 3: Load from found path, or fall back to system search + late find.
187 if find is not None: 1abgedjpcq
188 return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) 1pq
190 loaded = LOADER.load_with_system_search(desc) 1abgedjc
191 if loaded is not None: 1abgedjc
192 return loaded 1adj
194 find = run_find_steps(ctx, LATE_FIND_STEPS) 1abgec
195 if find is not None: 1abgec
196 return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via) 1ag
198 if desc.ctk_root_canary_anchor_libnames: 1bec
199 canary_abs_path = _try_ctk_root_canary(ctx) 1bc
200 if canary_abs_path is not None: 1bc
201 return LOADER.load_with_abs_path(desc, canary_abs_path, "system-ctk-root") 1b
203 ctx.raise_not_found() 1ec
206@functools.cache
207def load_nvidia_dynamic_lib(libname: str) -> LoadedDL:
208 """Load an NVIDIA dynamic library by name.
210 Args:
211 libname (str): The short name of the library to load (e.g., ``"cudart"``,
212 ``"nvvm"``, etc.).
214 Returns:
215 LoadedDL: Object containing the OS library handle and absolute path.
217 **Important:**
219 **Never close the returned handle.** Do **not** call ``dlclose`` (Linux) or
220 ``FreeLibrary`` (Windows) on the ``LoadedDL._handle_uint``.
222 **Why:** the return value is cached (``functools.cache``) and shared across the
223 process. Closing the handle can unload the module while other code still uses
224 it, leading to crashes or subtle failures.
226 This applies to Linux and Windows. For context, see issue #1011:
227 https://github.com/NVIDIA/cuda-python/issues/1011
229 Raises:
230 DynamicLibUnknownError: If ``libname`` is not a recognized library name.
231 DynamicLibNotAvailableError: If ``libname`` is recognized but not
232 supported on this platform.
233 DynamicLibNotFoundError: If the library cannot be found or loaded.
234 RuntimeError: If Python is not 64-bit.
236 Search order:
237 0. **Already loaded in the current process**
239 - If a matching library is already loaded by some other component,
240 return its absolute path and handle and skip the rest of the search.
242 1. **NVIDIA Python wheels**
244 - Scan installed distributions (``site-packages``) to find libraries
245 shipped in NVIDIA wheels.
247 2. **Conda environment**
249 - Conda installations are discovered via ``CONDA_PREFIX``, which is
250 defined automatically in activated conda environments (see
251 https://docs.conda.io/projects/conda-build/en/stable/user-guide/environment-variables.html).
253 3. **OS default mechanisms**
255 - Fall back to the native loader:
257 - Linux: ``dlopen()``
259 - Windows: ``LoadLibraryExW()``
261 On Linux, CUDA Toolkit (CTK) system installs with system config updates are
262 usually discovered via ``/etc/ld.so.conf.d/*cuda*.conf``.
264 On Windows, under Python 3.8+, CPython configures the process with
265 ``SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)``.
266 As a result, the native DLL search used here does **not** include
267 the system ``PATH``.
269 4. **Environment variables**
271 - If set, use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order).
272 On Windows, this is the typical way system-installed CTK DLLs are
273 located. Note that the NVIDIA CTK installer automatically
274 adds ``CUDA_PATH`` to the system-wide environment.
276 5. **CTK root canary probe (discoverable libs only)**
278 - For selected libraries whose shared object doesn't reside on the
279 standard linker path (currently ``nvvm``), attempt to derive CTK
280 root by system-loading a well-known CTK canary library in a
281 subprocess and then searching relative to that root. On Windows,
282 the canary uses the same native ``LoadLibraryExW`` semantics as
283 step 3, so there is also no ``PATH``-based discovery.
285 **Driver libraries** (``"cuda"``, ``"nvml"``):
287 These are part of the NVIDIA display driver (not the CUDA Toolkit) and
288 are expected to be reachable via the native OS loader path. For these
289 libraries the search is simplified to:
291 0. Already loaded in the current process
292 1. OS default mechanisms (``dlopen`` / ``LoadLibraryExW``)
294 The CTK-specific steps (site-packages, conda, ``CUDA_PATH``, canary
295 probe) are skipped entirely.
297 Notes:
298 The search is performed **per library**. There is currently no mechanism to
299 guarantee that multiple libraries are all resolved from the same location.
301 """
302 pointer_size_bits = struct.calcsize("P") * 8 1adfQ60
303 if pointer_size_bits != 64: 1adfQ60
304 raise RuntimeError( 16
305 f"cuda.pathfinder.load_nvidia_dynamic_lib() requires 64-bit Python."
306 f" Currently running: {pointer_size_bits}-bit Python"
307 f" {sys.version_info.major}.{sys.version_info.minor}"
308 )
309 if libname not in _ALL_KNOWN_LIBNAMES: 1adfQ0
310 raise DynamicLibUnknownError(f"Unknown library name: {libname!r}. Known names: {sorted(_ALL_KNOWN_LIBNAMES)}") 10
311 if libname not in _ALL_SUPPORTED_LIBNAMES: 1adfQ
312 raise DynamicLibNotAvailableError( 1Q
313 f"Library name {libname!r} is known but not available on {_PLATFORM_NAME}. "
314 f"Supported names on {_PLATFORM_NAME}: {sorted(_ALL_SUPPORTED_LIBNAMES)}"
315 )
316 return _load_lib_no_cache(libname) 1adf