diff --git a/cuda_bindings/cuda/bindings/_path_finder/README.md b/cuda_bindings/cuda/bindings/_path_finder/README.md new file mode 100644 index 000000000..fa51b56fa --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/README.md @@ -0,0 +1,55 @@ +# `cuda.bindings.path_finder` Module + +## Public API (Work in Progress) + +Currently exposes two primary interfaces: + +``` +cuda.bindings.path_finder._SUPPORTED_LIBNAMES # ('nvJitLink', 'nvrtc', 'nvvm') +cuda.bindings.path_finder._load_nvidia_dynamic_library(libname: str) -> LoadedDL +``` + +**Note:** +These APIs are prefixed with an underscore because they are considered +experimental while undergoing active development, although already +reasonably well-tested through CI pipelines. + +## Library Loading Search Priority + +The `load_nvidia_dynamic_library()` function implements a hierarchical search +strategy for locating NVIDIA shared libraries: + +0. **Check if a library was loaded into the process already by some other means.** + - If yes, there is no alternative to skipping the rest of the search logic. + The absolute path of the already loaded library will be returned, along + with the handle to the library. + +1. **NVIDIA Python wheels** + - Scans all site-packages to find libraries installed via NVIDIA Python wheels. + +2. **OS default mechanisms / Conda environments** + - Falls back to native loader: + - `dlopen()` on Linux + - `LoadLibraryW()` on Windows + - CTK installations with system config updates are expected to be discovered: + - Linux: Via `/etc/ld.so.conf.d/*cuda*.conf` + - Windows: Via `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y\bin` on system `PATH` + - Conda installations are expected to be discovered: + - Linux: Via `$ORIGIN/../lib` on `RPATH` (of the `python` binary) + - Windows: Via `%CONDA_PREFIX%\Library\bin` on system `PATH` + +3. **Environment variables** + - Relies on `CUDA_HOME` or `CUDA_PATH` environment variables if set + (in that order). + +Note that the search is done on a per-library basis. There is no centralized +mechanism that ensures all libraries are found in the same way. + +## Maintenance Requirements + +These key components must be updated for new CUDA Toolkit releases: + +- `supported_libs.SUPPORTED_LIBNAMES` +- `supported_libs.SUPPORTED_WINDOWS_DLLS` +- `supported_libs.SUPPORTED_LINUX_SONAMES` +- `supported_libs.EXPECTED_LIB_SYMBOLS` diff --git a/cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py b/cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py new file mode 100644 index 000000000..9835b72d0 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py @@ -0,0 +1,153 @@ +# Copyright 2024-2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import functools +import glob +import os + +from cuda.bindings._path_finder.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.bindings._path_finder.supported_libs import IS_WINDOWS, is_suppressed_dll_file + + +def _no_such_file_in_sub_dirs(sub_dirs, file_wild, error_messages, attachments): + error_messages.append(f"No such file: {file_wild}") + for sub_dir in find_sub_dirs_all_sitepackages(sub_dirs): + attachments.append(f' listdir("{sub_dir}"):') + for node in sorted(os.listdir(sub_dir)): + attachments.append(f" {node}") + + +def _find_so_using_nvidia_lib_dirs(libname, so_basename, error_messages, attachments): + nvidia_sub_dirs = ("nvidia", "*", "nvvm", "lib64") if libname == "nvvm" else ("nvidia", "*", "lib") + file_wild = so_basename + "*" + for lib_dir in find_sub_dirs_all_sitepackages(nvidia_sub_dirs): + # First look for an exact match + so_name = os.path.join(lib_dir, so_basename) + if os.path.isfile(so_name): + return so_name + # Look for a versioned library + # Using sort here mainly to make the result deterministic. + for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild))): + if os.path.isfile(so_name): + return so_name + _no_such_file_in_sub_dirs(nvidia_sub_dirs, file_wild, error_messages, attachments) + return None + + +def _find_dll_under_dir(dirpath, file_wild): + for path in sorted(glob.glob(os.path.join(dirpath, file_wild))): + if not os.path.isfile(path): + continue + if not is_suppressed_dll_file(os.path.basename(path)): + return path + return None + + +def _find_dll_using_nvidia_bin_dirs(libname, lib_searched_for, error_messages, attachments): + nvidia_sub_dirs = ("nvidia", "*", "nvvm", "bin") if libname == "nvvm" else ("nvidia", "*", "bin") + for bin_dir in find_sub_dirs_all_sitepackages(nvidia_sub_dirs): + dll_name = _find_dll_under_dir(bin_dir, lib_searched_for) + if dll_name is not None: + return dll_name + _no_such_file_in_sub_dirs(nvidia_sub_dirs, lib_searched_for, error_messages, attachments) + return None + + +def _get_cuda_home(): + cuda_home = os.environ.get("CUDA_HOME") + if cuda_home is None: + cuda_home = os.environ.get("CUDA_PATH") + return cuda_home + + +def _find_lib_dir_using_cuda_home(libname): + cuda_home = _get_cuda_home() + if cuda_home is None: + return None + if IS_WINDOWS: + subdirs = (os.path.join("nvvm", "bin"),) if libname == "nvvm" else ("bin",) + else: + subdirs = ( + (os.path.join("nvvm", "lib64"),) + if libname == "nvvm" + else ( + "lib64", # CTK + "lib", # Conda + ) + ) + for subdir in subdirs: + dirname = os.path.join(cuda_home, subdir) + if os.path.isdir(dirname): + return dirname + return None + + +def _find_so_using_lib_dir(lib_dir, so_basename, error_messages, attachments): + so_name = os.path.join(lib_dir, so_basename) + if os.path.isfile(so_name): + return so_name + error_messages.append(f"No such file: {so_name}") + attachments.append(f' listdir("{lib_dir}"):') + if not os.path.isdir(lib_dir): + attachments.append(" DIRECTORY DOES NOT EXIST") + else: + for node in sorted(os.listdir(lib_dir)): + attachments.append(f" {node}") + return None + + +def _find_dll_using_lib_dir(lib_dir, libname, error_messages, attachments): + file_wild = libname + "*.dll" + dll_name = _find_dll_under_dir(lib_dir, file_wild) + if dll_name is not None: + return dll_name + error_messages.append(f"No such file: {file_wild}") + attachments.append(f' listdir("{lib_dir}"):') + for node in sorted(os.listdir(lib_dir)): + attachments.append(f" {node}") + return None + + +class _find_nvidia_dynamic_library: + def __init__(self, libname: str): + self.libname = libname + self.error_messages = [] + self.attachments = [] + self.abs_path = None + + if IS_WINDOWS: + self.lib_searched_for = f"{libname}*.dll" + if self.abs_path is None: + self.abs_path = _find_dll_using_nvidia_bin_dirs( + libname, self.lib_searched_for, self.error_messages, self.attachments + ) + else: + self.lib_searched_for = f"lib{libname}.so" + if self.abs_path is None: + self.abs_path = _find_so_using_nvidia_lib_dirs( + libname, self.lib_searched_for, self.error_messages, self.attachments + ) + + def retry_with_cuda_home_priority_last(self): + cuda_home_lib_dir = _find_lib_dir_using_cuda_home(self.libname) + if cuda_home_lib_dir is not None: + if IS_WINDOWS: + self.abs_path = _find_dll_using_lib_dir( + cuda_home_lib_dir, self.libname, self.error_messages, self.attachments + ) + else: + self.abs_path = _find_so_using_lib_dir( + cuda_home_lib_dir, self.lib_searched_for, self.error_messages, self.attachments + ) + + def raise_if_abs_path_is_None(self): + if self.abs_path: + return self.abs_path + err = ", ".join(self.error_messages) + att = "\n".join(self.attachments) + raise RuntimeError(f'Failure finding "{self.lib_searched_for}": {err}\n{att}') + + +@functools.cache +def find_nvidia_dynamic_library(libname: str) -> str: + return _find_nvidia_dynamic_library(libname).raise_if_abs_path_is_None() diff --git a/cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py b/cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py new file mode 100644 index 000000000..810132625 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py @@ -0,0 +1,52 @@ +# Copyright 2024-2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import functools +import os +import site +import sys + + +def find_sub_dirs_no_cache(parent_dirs, sub_dirs): + results = [] + for base in parent_dirs: + stack = [(base, 0)] # (current_path, index into sub_dirs) + while stack: + current_path, idx = stack.pop() + if idx == len(sub_dirs): + if os.path.isdir(current_path): + results.append(current_path) + continue + + sub = sub_dirs[idx] + if sub == "*": + try: + entries = sorted(os.listdir(current_path)) + except OSError: + continue + for entry in entries: + entry_path = os.path.join(current_path, entry) + if os.path.isdir(entry_path): + stack.append((entry_path, idx + 1)) + else: + next_path = os.path.join(current_path, sub) + if os.path.isdir(next_path): + stack.append((next_path, idx + 1)) + return results + + +@functools.cache +def find_sub_dirs_cached(parent_dirs, sub_dirs): + return find_sub_dirs_no_cache(parent_dirs, sub_dirs) + + +def find_sub_dirs(parent_dirs, sub_dirs): + return find_sub_dirs_cached(tuple(parent_dirs), tuple(sub_dirs)) + + +def find_sub_dirs_sys_path(sub_dirs): + return find_sub_dirs(sys.path, sub_dirs) + + +def find_sub_dirs_all_sitepackages(sub_dirs): + return find_sub_dirs((site.getusersitepackages(),) + tuple(site.getsitepackages()), sub_dirs) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py b/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py new file mode 100644 index 000000000..034b9d433 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py @@ -0,0 +1,36 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +from dataclasses import dataclass +from typing import Callable, Optional + +from cuda.bindings._path_finder.supported_libs import DIRECT_DEPENDENCIES, IS_WINDOWS + +if IS_WINDOWS: + import pywintypes + + HandleType = pywintypes.HANDLE +else: + HandleType = int + + +@dataclass +class LoadedDL: + handle: HandleType + abs_path: Optional[str] + was_already_loaded_from_elsewhere: bool + + +def load_dependencies(libname: str, load_func: Callable[[str], LoadedDL]) -> None: + """Load all dependencies for a given library. + + Args: + libname: The name of the library whose dependencies should be loaded + load_func: The function to use for loading libraries (e.g. load_nvidia_dynamic_library) + + Example: + >>> load_dependencies("cudart", load_nvidia_dynamic_library) + # This will load all dependencies of cudart using the provided loading function + """ + for dep in DIRECT_DEPENDENCIES.get(libname, ()): + load_func(dep) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py b/cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py new file mode 100644 index 000000000..b9f3839e1 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py @@ -0,0 +1,125 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import ctypes +import ctypes.util +import os +from typing import Optional + +from cuda.bindings._path_finder.load_dl_common import LoadedDL + +CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL + +LIBDL_PATH = ctypes.util.find_library("dl") or "libdl.so.2" +LIBDL = ctypes.CDLL(LIBDL_PATH) +LIBDL.dladdr.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +LIBDL.dladdr.restype = ctypes.c_int + + +class Dl_info(ctypes.Structure): + """Structure used by dladdr to return information about a loaded symbol.""" + + _fields_ = [ + ("dli_fname", ctypes.c_char_p), # path to .so + ("dli_fbase", ctypes.c_void_p), + ("dli_sname", ctypes.c_char_p), + ("dli_saddr", ctypes.c_void_p), + ] + + +def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> Optional[str]: + """Get the absolute path of a loaded dynamic library on Linux. + + Args: + libname: The name of the library + handle: The library handle + + Returns: + The absolute path to the library file, or None if no expected symbol is found + + Raises: + OSError: If dladdr fails to get information about the symbol + """ + from cuda.bindings._path_finder.supported_libs import EXPECTED_LIB_SYMBOLS + + for symbol_name in EXPECTED_LIB_SYMBOLS[libname]: + symbol = getattr(handle, symbol_name, None) + if symbol is not None: + break + else: + return None + + addr = ctypes.cast(symbol, ctypes.c_void_p) + info = Dl_info() + if LIBDL.dladdr(addr, ctypes.byref(info)) == 0: + raise OSError(f"dladdr failed for {libname=!r}") + return info.dli_fname.decode() + + +def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: + """Check if the library is already loaded in the process. + + Args: + libname: The name of the library to check + + Returns: + A LoadedDL object if the library is already loaded, None otherwise + + Example: + >>> loaded = check_if_already_loaded_from_elsewhere("cudart") + >>> if loaded is not None: + ... print(f"Library already loaded from {loaded.abs_path}") + """ + from cuda.bindings._path_finder.supported_libs import SUPPORTED_LINUX_SONAMES + + for soname in SUPPORTED_LINUX_SONAMES.get(libname, ()): + try: + handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD) + except OSError: + continue + else: + return LoadedDL(handle._handle, abs_path_for_dynamic_library(libname, handle), True) + return None + + +def load_with_system_search(libname: str, soname: str) -> Optional[LoadedDL]: + """Try to load a library using system search paths. + + Args: + libname: The name of the library to load + soname: The soname to search for + + Returns: + A LoadedDL object if successful, None if the library cannot be loaded + + Raises: + RuntimeError: If the library is loaded but no expected symbol is found + """ + try: + handle = ctypes.CDLL(soname, CDLL_MODE) + abs_path = abs_path_for_dynamic_library(libname, handle) + if abs_path is None: + raise RuntimeError(f"No expected symbol for {libname=!r}") + return LoadedDL(handle._handle, abs_path, False) + except OSError: + return None + + +def load_with_abs_path(libname: str, found_path: str) -> LoadedDL: + """Load a dynamic library from the given path. + + Args: + libname: The name of the library to load + found_path: The absolute path to the library file + + Returns: + A LoadedDL object representing the loaded library + + Raises: + RuntimeError: If the library cannot be loaded + """ + try: + handle = ctypes.CDLL(found_path, CDLL_MODE) + except OSError as e: + raise RuntimeError(f"Failed to dlopen {found_path}: {e}") from e + return LoadedDL(handle._handle, found_path, False) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py b/cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py new file mode 100644 index 000000000..ec305be92 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py @@ -0,0 +1,153 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import ctypes +import ctypes.wintypes +from typing import Optional + +import pywintypes +import win32api + +from cuda.bindings._path_finder.load_dl_common import LoadedDL + +# Mirrors WinBase.h (unfortunately not defined already elsewhere) +WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100 +WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000 + + +def add_dll_directory(dll_abs_path: str) -> None: + """Add a DLL directory to the search path and update PATH environment variable. + + Args: + dll_abs_path: Absolute path to the DLL file + + Raises: + AssertionError: If the directory containing the DLL does not exist + """ + import os + + dirpath = os.path.dirname(dll_abs_path) + assert os.path.isdir(dirpath), dll_abs_path + # Add the DLL directory to the search path + os.add_dll_directory(dirpath) + # Update PATH as a fallback for dependent DLL resolution + curr_path = os.environ.get("PATH") + os.environ["PATH"] = dirpath if curr_path is None else os.pathsep.join((curr_path, dirpath)) + + +def abs_path_for_dynamic_library(libname: str, handle: pywintypes.HANDLE) -> str: + """Get the absolute path of a loaded dynamic library on Windows. + + Args: + handle: The library handle + + Returns: + The absolute path to the DLL file + + Raises: + OSError: If GetModuleFileNameW fails + RuntimeError: If the required path length is unreasonably long + """ + MAX_ITERATIONS = 10 # Allows for extremely long paths (up to ~266,000 chars) + buf_size = 260 # Start with traditional MAX_PATH + + for _ in range(MAX_ITERATIONS): + buf = ctypes.create_unicode_buffer(buf_size) + n_chars = ctypes.windll.kernel32.GetModuleFileNameW(ctypes.wintypes.HMODULE(handle), buf, buf_size) + + if n_chars == 0: + raise OSError( + f"GetModuleFileNameW failed ({libname=!r}, {buf_size=}). " + "Long paths may require enabling the " + "Windows 10+ long path registry setting. See: " + "https://docs.python.org/3/using/windows.html#removing-the-max-path-limitation" + ) + if n_chars < buf_size - 1: + return buf.value + + buf_size *= 2 # Double the buffer size and try again + + raise RuntimeError( + f"Failed to retrieve the full path after {MAX_ITERATIONS} attempts " + f"(final buffer size: {buf_size} characters). " + "This may indicate:\n" + " 1. An extremely long path requiring Windows long path support, or\n" + " 2. An invalid or corrupt library handle, or\n" + " 3. An unexpected system error.\n" + "See: https://docs.python.org/3/using/windows.html#removing-the-max-path-limitation" + ) + + +def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: + """Check if the library is already loaded in the process. + + Args: + libname: The name of the library to check + + Returns: + A LoadedDL object if the library is already loaded, None otherwise + + Example: + >>> loaded = check_if_already_loaded_from_elsewhere("cudart") + >>> if loaded is not None: + ... print(f"Library already loaded from {loaded.abs_path}") + """ + from cuda.bindings._path_finder.supported_libs import SUPPORTED_WINDOWS_DLLS + + for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()): + try: + handle = win32api.GetModuleHandle(dll_name) + except pywintypes.error: + continue + else: + return LoadedDL(handle, abs_path_for_dynamic_library(libname, handle), True) + return None + + +def load_with_system_search(libname: str, _unused: str) -> Optional[LoadedDL]: + """Try to load a DLL using system search paths. + + Args: + libname: The name of the library to load + _unused: Unused parameter (kept for interface consistency) + + Returns: + A LoadedDL object if successful, None if the library cannot be loaded + """ + from cuda.bindings._path_finder.supported_libs import SUPPORTED_WINDOWS_DLLS + + for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()): + try: + handle = win32api.LoadLibraryEx(dll_name, 0, 0) + except pywintypes.error: + continue + else: + return LoadedDL(handle, abs_path_for_dynamic_library(libname, handle), False) + + return None + + +def load_with_abs_path(libname: str, found_path: str) -> LoadedDL: + """Load a dynamic library from the given path. + + Args: + libname: The name of the library to load + found_path: The absolute path to the DLL file + + Returns: + A LoadedDL object representing the loaded library + + Raises: + RuntimeError: If the DLL cannot be loaded + """ + from cuda.bindings._path_finder.supported_libs import LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY + + if libname in LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY: + add_dll_directory(found_path) + + flags = WINBASE_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + try: + handle = win32api.LoadLibraryEx(found_path, 0, flags) + except pywintypes.error as e: + raise RuntimeError(f"Failed to load DLL at {found_path}: {e}") from e + return LoadedDL(handle, found_path, False) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py b/cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py new file mode 100644 index 000000000..f8fe5ce4a --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py @@ -0,0 +1,61 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import functools + +from cuda.bindings._path_finder.find_nvidia_dynamic_library import _find_nvidia_dynamic_library +from cuda.bindings._path_finder.load_dl_common import LoadedDL, load_dependencies +from cuda.bindings._path_finder.supported_libs import IS_WINDOWS + +if IS_WINDOWS: + from cuda.bindings._path_finder.load_dl_windows import ( + check_if_already_loaded_from_elsewhere, + load_with_abs_path, + load_with_system_search, + ) +else: + from cuda.bindings._path_finder.load_dl_linux import ( + check_if_already_loaded_from_elsewhere, + load_with_abs_path, + load_with_system_search, + ) + + +def _load_nvidia_dynamic_library_no_cache(libname: str) -> LoadedDL: + # Check whether the library is already loaded into the current process by + # some other component. This check uses OS-level mechanisms (e.g., + # dlopen on Linux, GetModuleHandle on Windows). + loaded = check_if_already_loaded_from_elsewhere(libname) + if loaded is not None: + return loaded + + # Load dependencies first + load_dependencies(libname, load_nvidia_dynamic_library) + + # Find the library path + found = _find_nvidia_dynamic_library(libname) + if found.abs_path is None: + loaded = load_with_system_search(libname, found.lib_searched_for) + if loaded is not None: + return loaded + found.retry_with_cuda_home_priority_last() + found.raise_if_abs_path_is_None() + + # Load the library from the found path + return load_with_abs_path(libname, found.abs_path) + + +@functools.cache +def load_nvidia_dynamic_library(libname: str) -> LoadedDL: + """Load a NVIDIA dynamic library by name. + + Args: + libname: The name of the library to load (e.g. "cudart", "nvvm", etc.) + + Returns: + A LoadedDL object containing the library handle and path + + Raises: + RuntimeError: If the library cannot be found or loaded + """ + return _load_nvidia_dynamic_library_no_cache(libname) diff --git a/cuda_bindings/cuda/bindings/_path_finder/supported_libs.py b/cuda_bindings/cuda/bindings/_path_finder/supported_libs.py new file mode 100644 index 000000000..14dc98a96 --- /dev/null +++ b/cuda_bindings/cuda/bindings/_path_finder/supported_libs.py @@ -0,0 +1,398 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +# THIS FILE NEEDS TO BE REVIEWED/UPDATED FOR EACH CTK RELEASE + +import sys + +IS_WINDOWS = sys.platform == "win32" + +SUPPORTED_LIBNAMES = ( + # Core CUDA Runtime and Compiler + "nvJitLink", + "nvrtc", + "nvvm", +) + +PARTIALLY_SUPPORTED_LIBNAMES_COMMON = ( + # Core CUDA Runtime and Compiler + "cudart", + "nvfatbin", + # Math Libraries + "cublas", + "cublasLt", + "cufft", + "cufftw", + "curand", + "cusolver", + "cusolverMg", + "cusparse", + "nppc", + "nppial", + "nppicc", + "nppidei", + "nppif", + "nppig", + "nppim", + "nppist", + "nppisu", + "nppitc", + "npps", + "nvblas", + # Other + "nvjpeg", +) + +# Note: The `cufile_rdma` information is intentionally retained (commented out) +# despite not being actively used in the current build. It took a nontrivial +# amount of effort to determine the SONAME, dependencies, and expected symbols +# for this special-case library, especially given its RDMA/MLX5 dependencies +# and limited availability. Keeping this as a reference avoids having to +# reconstruct the information from scratch in the future. + +PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY = ( + "cufile", + # "cufile_rdma", # Requires libmlx5.so +) + +PARTIALLY_SUPPORTED_LIBNAMES_LINUX = PARTIALLY_SUPPORTED_LIBNAMES_COMMON + PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY + +PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY = () + +PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS = PARTIALLY_SUPPORTED_LIBNAMES_COMMON + PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY + +PARTIALLY_SUPPORTED_LIBNAMES_ALL = ( + PARTIALLY_SUPPORTED_LIBNAMES_COMMON + + PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY + + PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY +) + +if IS_WINDOWS: + PARTIALLY_SUPPORTED_LIBNAMES = PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS +else: + PARTIALLY_SUPPORTED_LIBNAMES = PARTIALLY_SUPPORTED_LIBNAMES_LINUX + +# Based on ldd output for Linux x86_64 nvidia-*-cu12 wheels (12.8.1) +DIRECT_DEPENDENCIES = { + "cublas": ("cublasLt",), + "cufftw": ("cufft",), + # "cufile_rdma": ("cufile",), + "cusolver": ("nvJitLink", "cusparse", "cublasLt", "cublas"), + "cusolverMg": ("nvJitLink", "cublasLt", "cublas"), + "cusparse": ("nvJitLink",), + "nppial": ("nppc",), + "nppicc": ("nppc",), + "nppidei": ("nppc",), + "nppif": ("nppc",), + "nppig": ("nppc",), + "nppim": ("nppc",), + "nppist": ("nppc",), + "nppisu": ("nppc",), + "nppitc": ("nppc",), + "npps": ("nppc",), + "nvblas": ("cublas", "cublasLt"), +} + +# Based on these released files: +# cuda_11.0.3_450.51.06_linux.run +# cuda_11.1.1_455.32.00_linux.run +# cuda_11.2.2_460.32.03_linux.run +# cuda_11.3.1_465.19.01_linux.run +# cuda_11.4.4_470.82.01_linux.run +# cuda_11.5.1_495.29.05_linux.run +# cuda_11.6.2_510.47.03_linux.run +# cuda_11.7.1_515.65.01_linux.run +# cuda_11.8.0_520.61.05_linux.run +# cuda_12.0.1_525.85.12_linux.run +# cuda_12.1.1_530.30.02_linux.run +# cuda_12.2.2_535.104.05_linux.run +# cuda_12.3.2_545.23.08_linux.run +# cuda_12.4.1_550.54.15_linux.run +# cuda_12.5.1_555.42.06_linux.run +# cuda_12.6.2_560.35.03_linux.run +# cuda_12.8.0_570.86.10_linux.run +# cuda_12.9.0_575.51.03_linux.run +# Generated with toolshed/build_path_finder_sonames.py +SUPPORTED_LINUX_SONAMES = { + "cublas": ( + "libcublas.so.11", + "libcublas.so.12", + ), + "cublasLt": ( + "libcublasLt.so.11", + "libcublasLt.so.12", + ), + "cudart": ( + "libcudart.so.11.0", + "libcudart.so.12", + ), + "cufft": ( + "libcufft.so.10", + "libcufft.so.11", + ), + "cufftw": ( + "libcufftw.so.10", + "libcufftw.so.11", + ), + "cufile": ("libcufile.so.0",), + # "cufile_rdma": ("libcufile_rdma.so.1",), + "curand": ("libcurand.so.10",), + "cusolver": ( + "libcusolver.so.10", + "libcusolver.so.11", + ), + "cusolverMg": ( + "libcusolverMg.so.10", + "libcusolverMg.so.11", + ), + "cusparse": ( + "libcusparse.so.11", + "libcusparse.so.12", + ), + "nppc": ( + "libnppc.so.11", + "libnppc.so.12", + ), + "nppial": ( + "libnppial.so.11", + "libnppial.so.12", + ), + "nppicc": ( + "libnppicc.so.11", + "libnppicc.so.12", + ), + "nppidei": ( + "libnppidei.so.11", + "libnppidei.so.12", + ), + "nppif": ( + "libnppif.so.11", + "libnppif.so.12", + ), + "nppig": ( + "libnppig.so.11", + "libnppig.so.12", + ), + "nppim": ( + "libnppim.so.11", + "libnppim.so.12", + ), + "nppist": ( + "libnppist.so.11", + "libnppist.so.12", + ), + "nppisu": ( + "libnppisu.so.11", + "libnppisu.so.12", + ), + "nppitc": ( + "libnppitc.so.11", + "libnppitc.so.12", + ), + "npps": ( + "libnpps.so.11", + "libnpps.so.12", + ), + "nvJitLink": ("libnvJitLink.so.12",), + "nvblas": ( + "libnvblas.so.11", + "libnvblas.so.12", + ), + "nvfatbin": ("libnvfatbin.so.12",), + "nvjpeg": ( + "libnvjpeg.so.11", + "libnvjpeg.so.12", + ), + "nvrtc": ( + "libnvrtc.so.11.0", + "libnvrtc.so.11.1", + "libnvrtc.so.11.2", + "libnvrtc.so.12", + ), + "nvvm": ( + "libnvvm.so.3", + "libnvvm.so.4", + ), +} + +# Based on these released files: +# cuda_11.0.3_451.82_win10.exe +# cuda_11.1.1_456.81_win10.exe +# cuda_11.2.2_461.33_win10.exe +# cuda_11.3.1_465.89_win10.exe +# cuda_11.4.4_472.50_windows.exe +# cuda_11.5.1_496.13_windows.exe +# cuda_11.6.2_511.65_windows.exe +# cuda_11.7.1_516.94_windows.exe +# cuda_11.8.0_522.06_windows.exe +# cuda_12.0.1_528.33_windows.exe +# cuda_12.1.1_531.14_windows.exe +# cuda_12.2.2_537.13_windows.exe +# cuda_12.3.2_546.12_windows.exe +# cuda_12.4.1_551.78_windows.exe +# cuda_12.5.1_555.85_windows.exe +# cuda_12.6.2_560.94_windows.exe +# cuda_12.8.1_572.61_windows.exe +# cuda_12.9.0_576.02_windows.txt +# Generated with toolshed/build_path_finder_dlls.py (WITH MANUAL EDITS) +SUPPORTED_WINDOWS_DLLS = { + "cublas": ( + "cublas64_11.dll", + "cublas64_12.dll", + ), + "cublasLt": ( + "cublasLt64_11.dll", + "cublasLt64_12.dll", + ), + "cudart": ( + "cudart32_110.dll", + "cudart32_65.dll", + "cudart32_90.dll", + "cudart64_101.dll", + "cudart64_110.dll", + "cudart64_12.dll", + "cudart64_65.dll", + ), + "cufft": ( + "cufft64_10.dll", + "cufft64_11.dll", + "cufftw64_10.dll", + "cufftw64_11.dll", + ), + "cufftw": ( + "cufftw64_10.dll", + "cufftw64_11.dll", + ), + "curand": ("curand64_10.dll",), + "cusolver": ( + "cusolver64_10.dll", + "cusolver64_11.dll", + ), + "cusolverMg": ( + "cusolverMg64_10.dll", + "cusolverMg64_11.dll", + ), + "cusparse": ( + "cusparse64_11.dll", + "cusparse64_12.dll", + ), + "nppc": ( + "nppc64_11.dll", + "nppc64_12.dll", + ), + "nppial": ( + "nppial64_11.dll", + "nppial64_12.dll", + ), + "nppicc": ( + "nppicc64_11.dll", + "nppicc64_12.dll", + ), + "nppidei": ( + "nppidei64_11.dll", + "nppidei64_12.dll", + ), + "nppif": ( + "nppif64_11.dll", + "nppif64_12.dll", + ), + "nppig": ( + "nppig64_11.dll", + "nppig64_12.dll", + ), + "nppim": ( + "nppim64_11.dll", + "nppim64_12.dll", + ), + "nppist": ( + "nppist64_11.dll", + "nppist64_12.dll", + ), + "nppisu": ( + "nppisu64_11.dll", + "nppisu64_12.dll", + ), + "nppitc": ( + "nppitc64_11.dll", + "nppitc64_12.dll", + ), + "npps": ( + "npps64_11.dll", + "npps64_12.dll", + ), + "nvJitLink": ("nvJitLink_120_0.dll",), + "nvblas": ( + "nvblas64_11.dll", + "nvblas64_12.dll", + ), + "nvfatbin": ("nvfatbin_120_0.dll",), + "nvjpeg": ( + "nvjpeg64_11.dll", + "nvjpeg64_12.dll", + ), + "nvrtc": ( + "nvrtc64_110_0.dll", + "nvrtc64_111_0.dll", + "nvrtc64_112_0.dll", + "nvrtc64_120_0.dll", + ), + "nvvm": ( + "nvvm32.dll", + "nvvm64.dll", + "nvvm64_33_0.dll", + "nvvm64_40_0.dll", + "nvvm70.dll", + ), +} + +LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY = ( + "cufft", + "nvrtc", +) + + +def is_suppressed_dll_file(path_basename: str) -> bool: + if path_basename.startswith("nvrtc"): + # nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl: + # nvidia\cuda_nvrtc\bin\ + # nvrtc-builtins64_128.dll + # nvrtc64_120_0.alt.dll + # nvrtc64_120_0.dll + return path_basename.endswith(".alt.dll") or "-builtins" in path_basename + return False + + +# Based on nm output for Linux x86_64 /usr/local/cuda (12.8.1) +EXPECTED_LIB_SYMBOLS = { + "nvJitLink": ( + "__nvJitLinkCreate_12_0", # 12.0 through 12.8 (at least) + "nvJitLinkVersion", # 12.3 and up + ), + "nvrtc": ("nvrtcVersion",), + "nvvm": ("nvvmVersion",), + "cudart": ("cudaRuntimeGetVersion",), + "nvfatbin": ("nvFatbinVersion",), + "cublas": ("cublasGetVersion",), + "cublasLt": ("cublasLtGetVersion",), + "cufft": ("cufftGetVersion",), + "cufftw": ("fftwf_malloc",), + "curand": ("curandGetVersion",), + "cusolver": ("cusolverGetVersion",), + "cusolverMg": ("cusolverMgCreate",), + "cusparse": ("cusparseGetVersion",), + "nppc": ("nppGetLibVersion",), + "nppial": ("nppiAdd_32f_C1R",), + "nppicc": ("nppiColorToGray_8u_C3C1R",), + "nppidei": ("nppiCopy_8u_C1R",), + "nppif": ("nppiFilterSobelHorizBorder_8u_C1R",), + "nppig": ("nppiResize_8u_C1R",), + "nppim": ("nppiErode_8u_C1R",), + "nppist": ("nppiMean_8u_C1R",), + "nppisu": ("nppiFree",), + "nppitc": ("nppiThreshold_8u_C1R",), + "npps": ("nppsAdd_32f",), + "nvblas": ("dgemm",), + "cufile": ("cuFileGetVersion",), + # "cufile_rdma": ("rdma_buffer_reg",), + "nvjpeg": ("nvjpegCreate",), +} diff --git a/cuda_bindings/cuda/bindings/path_finder.py b/cuda_bindings/cuda/bindings/path_finder.py new file mode 100644 index 000000000..28badd025 --- /dev/null +++ b/cuda_bindings/cuda/bindings/path_finder.py @@ -0,0 +1,13 @@ +# Copyright 2024-2025 NVIDIA Corporation. All rights reserved. +# +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +from cuda.bindings._path_finder.load_nvidia_dynamic_library import ( + load_nvidia_dynamic_library as _load_nvidia_dynamic_library, +) +from cuda.bindings._path_finder.supported_libs import SUPPORTED_LIBNAMES as _SUPPORTED_LIBNAMES + +__all__ = [ + "_load_nvidia_dynamic_library", + "_SUPPORTED_LIBNAMES", +] diff --git a/cuda_bindings/tests/conftest.py b/cuda_bindings/tests/conftest.py index 45767fb78..fa6293cc7 100644 --- a/cuda_bindings/tests/conftest.py +++ b/cuda_bindings/tests/conftest.py @@ -1,3 +1,6 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + import os import pytest @@ -6,3 +9,22 @@ os.environ.get("CUDA_PYTHON_TESTING_WITH_COMPUTE_SANITIZER", "0") == "1", reason="The compute-sanitizer is running, and this test causes an API error.", ) + + +def pytest_configure(config): + config.custom_info = [] + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + if config.custom_info: + terminalreporter.write_sep("=", "INFO summary") + for msg in config.custom_info: + terminalreporter.line(f"INFO {msg}") + + +@pytest.fixture +def info_summary_append(request): + def _append(message): + request.config.custom_info.append(f"{request.node.name}: {message}") + + return _append diff --git a/cuda_bindings/tests/run_python_code_safely.py b/cuda_bindings/tests/run_python_code_safely.py new file mode 100644 index 000000000..349ed9682 --- /dev/null +++ b/cuda_bindings/tests/run_python_code_safely.py @@ -0,0 +1,89 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import multiprocessing +import queue # for Empty +import subprocess # nosec B404 +import sys +import traceback +from io import StringIO + + +class Worker: + def __init__(self, python_code, result_queue): + self.python_code = python_code + self.result_queue = result_queue + + def __call__(self): + # Capture stdout/stderr + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = StringIO() + sys.stderr = StringIO() + + try: + exec(self.python_code, {"__name__": "__main__"}) # nosec B102 + returncode = 0 + except SystemExit as e: # Handle sys.exit() + returncode = e.code if isinstance(e.code, int) else 0 + except BaseException: + traceback.print_exc() + returncode = 1 + finally: + # Collect outputs and restore streams + stdout = sys.stdout.getvalue() + stderr = sys.stderr.getvalue() + sys.stdout = old_stdout + sys.stderr = old_stderr + try: # noqa: SIM105 + self.result_queue.put((returncode, stdout, stderr)) + except Exception: # nosec B110 + # If the queue is broken (e.g., parent gone), best effort logging + pass + + +def run_python_code_safely(python_code, *, timeout=None): + """Run Python code in a spawned subprocess, capturing stdout/stderr/output.""" + ctx = multiprocessing.get_context("spawn") + result_queue = ctx.Queue() + process = ctx.Process(target=Worker(python_code, result_queue)) + process.start() + + try: + process.join(timeout) + if process.is_alive(): + process.terminate() + process.join() + return subprocess.CompletedProcess( + args=[sys.executable, "-c", python_code], + returncode=-9, + stdout="", + stderr=f"Process timed out after {timeout} seconds and was terminated.", + ) + + try: + returncode, stdout, stderr = result_queue.get(timeout=1.0) + except (queue.Empty, EOFError): + return subprocess.CompletedProcess( + args=[sys.executable, "-c", python_code], + returncode=-999, + stdout="", + stderr="Process exited or crashed before returning results.", + ) + + return subprocess.CompletedProcess( + args=[sys.executable, "-c", python_code], + returncode=returncode, + stdout=stdout, + stderr=stderr, + ) + + finally: + try: + result_queue.close() + result_queue.join_thread() + except Exception: # nosec B110 + pass + if process.is_alive(): + process.kill() + process.join() diff --git a/cuda_bindings/tests/test_path_finder_find_sub_dirs.py b/cuda_bindings/tests/test_path_finder_find_sub_dirs.py new file mode 100644 index 000000000..6b2644bff --- /dev/null +++ b/cuda_bindings/tests/test_path_finder_find_sub_dirs.py @@ -0,0 +1,91 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import os + +import pytest + +from cuda.bindings._path_finder.find_sub_dirs import ( + find_sub_dirs, + find_sub_dirs_all_sitepackages, + find_sub_dirs_sys_path, +) + +NONEXISTENT = "NonExistentE12DBF1Fbe948337576B5F1E88f60bb2" + + +@pytest.fixture +def test_tree(tmp_path): + # Build: + # tmp_path/ + # sys1/nvidia/foo/lib + # sys1/nvidia/bar/lib + # sys2/nvidia/baz/nvvm/lib64 + base = tmp_path + (base / "sys1" / "nvidia" / "foo" / "lib").mkdir(parents=True) + (base / "sys1" / "nvidia" / "bar" / "lib").mkdir(parents=True) + (base / "sys2" / "nvidia" / "baz" / "nvvm" / "lib64").mkdir(parents=True) + + return { + "parent_paths": ( + str(base / "sys1"), + str(base / "sys2"), + str(base / NONEXISTENT), + ), + "base": base, + } + + +def test_exact_match(test_tree): + parent_paths = test_tree["parent_paths"] + base = test_tree["base"] + result = find_sub_dirs(parent_paths, ("nvidia", "foo", "lib")) + expected = [str(base / "sys1" / "nvidia" / "foo" / "lib")] + assert result == expected + + +def test_single_wildcard(test_tree): + parent_paths = test_tree["parent_paths"] + base = test_tree["base"] + result = find_sub_dirs(parent_paths, ("nvidia", "*", "lib")) + expected = [ + str(base / "sys1" / "nvidia" / "bar" / "lib"), + str(base / "sys1" / "nvidia" / "foo" / "lib"), + ] + assert sorted(result) == sorted(expected) + + +def test_double_wildcard(test_tree): + parent_paths = test_tree["parent_paths"] + base = test_tree["base"] + result = find_sub_dirs(parent_paths, ("nvidia", "*", "nvvm", "lib64")) + expected = [str(base / "sys2" / "nvidia" / "baz" / "nvvm" / "lib64")] + assert result == expected + + +def test_no_match(test_tree): + parent_paths = test_tree["parent_paths"] + result = find_sub_dirs(parent_paths, (NONEXISTENT,)) + assert result == [] + + +def test_empty_parent_paths(): + result = find_sub_dirs((), ("nvidia", "*", "lib")) + assert result == [] + + +def test_empty_sub_dirs(test_tree): + parent_paths = test_tree["parent_paths"] + result = find_sub_dirs(parent_paths, ()) + expected = [p for p in parent_paths if os.path.isdir(p)] + assert sorted(result) == sorted(expected) + + +def test_find_sub_dirs_sys_path_no_math(): + result = find_sub_dirs_sys_path((NONEXISTENT,)) + assert result == [] + + +def test_find_sub_dirs_all_sitepackages_no_match(): + result = find_sub_dirs_all_sitepackages((NONEXISTENT,)) + assert result == [] diff --git a/cuda_bindings/tests/test_path_finder_load.py b/cuda_bindings/tests/test_path_finder_load.py new file mode 100644 index 000000000..5c21e8a05 --- /dev/null +++ b/cuda_bindings/tests/test_path_finder_load.py @@ -0,0 +1,83 @@ +# Copyright 2025 NVIDIA Corporation. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import os +import sys + +import pytest +from run_python_code_safely import run_python_code_safely + +from cuda.bindings import path_finder +from cuda.bindings._path_finder import supported_libs + +ALL_LIBNAMES = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_ALL +ALL_LIBNAMES_LINUX = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_LINUX +ALL_LIBNAMES_WINDOWS = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS +if os.environ.get("CUDA_BINDINGS_PATH_FINDER_TEST_ALL_LIBNAMES", False): + if sys.platform == "win32": + TEST_FIND_OR_LOAD_LIBNAMES = ALL_LIBNAMES_WINDOWS + else: + TEST_FIND_OR_LOAD_LIBNAMES = ALL_LIBNAMES_LINUX +else: + TEST_FIND_OR_LOAD_LIBNAMES = path_finder._SUPPORTED_LIBNAMES + + +def test_all_libnames_linux_sonames_consistency(): + assert tuple(sorted(ALL_LIBNAMES_LINUX)) == tuple(sorted(supported_libs.SUPPORTED_LINUX_SONAMES.keys())) + + +def test_all_libnames_windows_dlls_consistency(): + assert tuple(sorted(ALL_LIBNAMES_WINDOWS)) == tuple(sorted(supported_libs.SUPPORTED_WINDOWS_DLLS.keys())) + + +def test_all_libnames_libnames_requiring_os_add_dll_directory_consistency(): + assert not (set(supported_libs.LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY) - set(ALL_LIBNAMES_WINDOWS)) + + +def test_all_libnames_expected_lib_symbols_consistency(): + assert tuple(sorted(ALL_LIBNAMES)) == tuple(sorted(supported_libs.EXPECTED_LIB_SYMBOLS.keys())) + + +def build_subprocess_failed_for_libname_message(libname, result): + return ( + f"Subprocess failed for {libname=!r} with exit code {result.returncode}\n" + f"--- stdout-from-subprocess ---\n{result.stdout}\n" + f"--- stderr-from-subprocess ---\n{result.stderr}\n" + ) + + +@pytest.mark.parametrize("libname", TEST_FIND_OR_LOAD_LIBNAMES) +def test_find_or_load_nvidia_dynamic_library(info_summary_append, libname): + # We intentionally run each dynamic library operation in a subprocess + # to ensure isolation of global dynamic linking state (e.g., dlopen handles). + # Without subprocesses, loading/unloading libraries during testing could + # interfere across test cases and lead to nondeterministic or platform-specific failures. + # + # Defining the subprocess code snippets as strings ensures each subprocess + # runs a minimal, independent script tailored to the specific libname and API being tested. + code = f"""\ +import os +from cuda.bindings.path_finder import _load_nvidia_dynamic_library +from cuda.bindings._path_finder.load_nvidia_dynamic_library import _load_nvidia_dynamic_library_no_cache + +loaded_dl_fresh = _load_nvidia_dynamic_library({libname!r}) +if loaded_dl_fresh.was_already_loaded_from_elsewhere: + raise RuntimeError("loaded_dl_fresh.was_already_loaded_from_elsewhere") + +loaded_dl_from_cache = _load_nvidia_dynamic_library({libname!r}) +if loaded_dl_from_cache is not loaded_dl_fresh: + raise RuntimeError("loaded_dl_from_cache is not loaded_dl_fresh") + +loaded_dl_no_cache = _load_nvidia_dynamic_library_no_cache({libname!r}) +if not loaded_dl_no_cache.was_already_loaded_from_elsewhere: + raise RuntimeError("loaded_dl_no_cache.was_already_loaded_from_elsewhere") +if not os.path.samefile(loaded_dl_no_cache.abs_path, loaded_dl_fresh.abs_path): + raise RuntimeError(f"not os.path.samefile({{loaded_dl_no_cache.abs_path=!r}}, {{loaded_dl_fresh.abs_path=!r}})") + +print(f"{{loaded_dl_fresh.abs_path!r}}") +""" + result = run_python_code_safely(code, timeout=30) + if result.returncode == 0: + info_summary_append(f"abs_path={result.stdout.rstrip()}") + else: + raise RuntimeError(build_subprocess_failed_for_libname_message(libname, result)) diff --git a/toolshed/build_path_finder_dlls.py b/toolshed/build_path_finder_dlls.py new file mode 100755 index 000000000..be2db0d1f --- /dev/null +++ b/toolshed/build_path_finder_dlls.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 + +# Input for this script: .txt files generated with: +# for exe in *.exe; do 7z l $exe > "${exe%.exe}.txt"; done + +# The output of this script +# requires obvious manual edits to remove duplicates and unwanted dlls. + +import sys + +LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER = ( + "nvJitLink", + "nvrtc", + "nvvm", + "cudart", + "nvfatbin", + "cublas", + "cublasLt", + "cufft", + "cufftw", + "curand", + "cusolver", + "cusolverMg", + "cusparse", + "nppc", + "nppial", + "nppicc", + "nppidei", + "nppif", + "nppig", + "nppim", + "nppist", + "nppisu", + "nppitc", + "npps", + "nvblas", + "cufile", + "cufile_rdma", + "nvjpeg", +) + + +def run(args): + dlls_from_files = set() + for filename in args: + lines_iter = iter(open(filename).read().splitlines()) + for line in lines_iter: + if line.startswith("-------------------"): + break + else: + raise RuntimeError("------------------- NOT FOUND") + for line in lines_iter: + if line.startswith("-------------------"): + break + assert line[52] == " ", line + assert line[53] != " ", line + path = line[53:] + if path.endswith(".dll"): + dll = path.rsplit("/", 1)[1] + dlls_from_files.add(dll) + else: + raise RuntimeError("------------------- NOT FOUND") + + print("DLLs in scope of cuda.bindings.path_finder") + print("==========================================") + dlls_in_scope = set() + for libname in sorted(LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER): + print(f'"{libname}": (') + for dll in sorted(dlls_from_files): + if dll.startswith(libname): + dlls_in_scope.add(dll) + print(f' "{dll}",') + print("),") + print() + + print("DLLs out of scope") + print("=================") + for dll in sorted(dlls_from_files - dlls_in_scope): + print(dll) + print() + + +if __name__ == "__main__": + run(args=sys.argv[1:]) diff --git a/toolshed/build_path_finder_sonames.py b/toolshed/build_path_finder_sonames.py new file mode 100755 index 000000000..17b7dd7b3 --- /dev/null +++ b/toolshed/build_path_finder_sonames.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 + +# Input for this script: +# output of toolshed/find_sonames.sh + +# The output of this script +# is expected to be usable as-is. + +import sys + +LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER = ( + "nvJitLink", + "nvrtc", + "nvvm", + "cudart", + "nvfatbin", + "cublas", + "cublasLt", + "cufft", + "cufftw", + "curand", + "cusolver", + "cusolverMg", + "cusparse", + "nppc", + "nppial", + "nppicc", + "nppidei", + "nppif", + "nppig", + "nppim", + "nppist", + "nppisu", + "nppitc", + "npps", + "nvblas", + "cufile", + "cufile_rdma", + "nvjpeg", +) + + +def run(args): + assert len(args) == 1, "output-of-find_sonames.sh" + + sonames_from_file = set() + for line in open(args[0]).read().splitlines(): + flds = line.split() + assert len(flds) == 3, flds + if flds[-1] != "SONAME_NOT_SET": + sonames_from_file.add(flds[-1]) + + print("SONAMEs in scope of cuda.bindings.path_finder") + print("=============================================") + sonames_in_scope = set() + for libname in sorted(LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER): + print(f'"{libname}": (') + lib_so = "lib" + libname + ".so" + for soname in sorted(sonames_from_file): + if soname.startswith(lib_so): + sonames_in_scope.add(soname) + print(f' "{soname}",') + print("),") + print() + + print("SONAMEs out of scope") + print("====================") + for soname in sorted(sonames_from_file - sonames_in_scope): + print(soname) + print() + + +if __name__ == "__main__": + run(args=sys.argv[1:]) diff --git a/toolshed/find_sonames.sh b/toolshed/find_sonames.sh new file mode 100755 index 000000000..b742becf6 --- /dev/null +++ b/toolshed/find_sonames.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 + +find "$@" -type f -name '*.so*' -print0 | while IFS= read -r -d '' f; do + type=$(test -L "$f" && echo SYMLINK || echo FILE) + soname=$(readelf -d "$f" 2>/dev/null | awk '/SONAME/ {gsub(/[][]/, "", $5); print $5; exit}') + echo "$f $type ${soname:-SONAME_NOT_SET}" +done diff --git a/toolshed/run_cuda_bindings_path_finder.py b/toolshed/run_cuda_bindings_path_finder.py new file mode 100644 index 000000000..ca2193a81 --- /dev/null +++ b/toolshed/run_cuda_bindings_path_finder.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# SPDX-License-Identifier: Apache-2.0 + +import sys +import traceback + +from cuda.bindings import path_finder +from cuda.bindings._path_finder import supported_libs + +ALL_LIBNAMES = ( + path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES +) + + +def run(args): + if args: + libnames = args + else: + libnames = ALL_LIBNAMES + + for libname in libnames: + print(f"{libname=}") + try: + loaded_dl = path_finder._load_nvidia_dynamic_library(libname) + except Exception: + print(f"EXCEPTION for {libname=}:") + traceback.print_exc(file=sys.stdout) + else: + print(f" {loaded_dl.abs_path=!r}") + print(f" {loaded_dl.was_already_loaded_from_elsewhere=!r}") + print() + + +if __name__ == "__main__": + run(args=sys.argv[1:])