Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 58 additions & 40 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,78 @@
import ctypes
import ctypes.util
import os
from typing import Optional
from typing import Optional, cast

from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LINUX_SONAMES

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

def _load_libdl() -> ctypes.CDLL:
# In normal glibc-based Linux environments, find_library("dl") should return
# something like "libdl.so.2". In minimal or stripped-down environments
# (no ldconfig/gcc, incomplete linker cache), this can return None even
# though libdl is present. In that case, we fall back to the stable SONAME.
name = ctypes.util.find_library("dl") or "libdl.so.2"
try:
return ctypes.CDLL(name)
except OSError as e:
raise RuntimeError(f"Could not load {name!r} (required for dlinfo/dlerror on Linux)") from e

class DlInfo(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),
)
LIBDL = _load_libdl()

# dlinfo
LIBDL.dlinfo.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
LIBDL.dlinfo.restype = ctypes.c_int

def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> Optional[str]:
"""Get the absolute path of a loaded dynamic library on Linux.
# dlerror (thread-local error string; cleared after read)
LIBDL.dlerror.argtypes = []
LIBDL.dlerror.restype = ctypes.c_char_p

Args:
libname: The name of the library
handle: The library handle
# First appeared in 2004-era glibc. Universally correct on Linux for all practical purposes.
RTLD_DI_LINKMAP = 2
Comment thread
leofang marked this conversation as resolved.

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.pathfinder._dynamic_libs.supported_nvidia_libs import EXPECTED_LIB_SYMBOLS
def _dl_last_error() -> Optional[str]:
msg_bytes = cast(Optional[bytes], LIBDL.dlerror())
if not msg_bytes:
return None # no pending error
# Never raises; undecodable bytes are mapped to U+DC80..U+DCFF
return msg_bytes.decode("utf-8", "surrogateescape")


def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
lm_ptr = ctypes.c_void_p()
rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_LINKMAP, ctypes.byref(lm_ptr))
if rc != 0:
err = _dl_last_error()
raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
if not lm_ptr.value:
raise OSError(f"dlinfo returned NULL link_map pointer for {libname=!r}")

for symbol_name in EXPECTED_LIB_SYMBOLS[libname]:
symbol = getattr(handle, symbol_name, None)
if symbol is not None:
break
else:
return None
# l_name is the second field, right after l_addr (both pointer-sized)
l_name_field_addr = lm_ptr.value + ctypes.sizeof(ctypes.c_void_p)
l_name_addr = ctypes.c_void_p.from_address(l_name_field_addr).value
Comment thread
leofang marked this conversation as resolved.
Outdated
if not l_name_addr:
raise OSError(f"dlinfo returned NULL link_map->l_name for {libname=!r}")
l_name = ctypes.string_at(l_name_addr) # bytes up to NUL
if not l_name:
raise OSError(f"dlinfo returned empty l_name for {libname=!r}")

addr = ctypes.cast(symbol, ctypes.c_void_p)
info = DlInfo()
if LIBDL.dladdr(addr, ctypes.byref(info)) == 0:
raise OSError(f"dladdr failed for {libname=!r}")
return info.dli_fname.decode() # type: ignore[no-any-return]
# Won't raise, and preserves undecodable bytes round-trip
path = os.fsdecode(l_name) # filesystem encoding + surrogateescape
if not path:
raise OSError(f"dlinfo returned empty path string for {libname=!r}")

return path


def get_candidate_sonames(libname: str) -> list[str]:
candidate_sonames = list(SUPPORTED_LINUX_SONAMES.get(libname, ()))
candidate_sonames.append(f"lib{libname}.so")
return candidate_sonames


def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
Expand All @@ -72,9 +93,8 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
>>> if loaded is not None:
... print(f"Library already loaded from {loaded.abs_path}")
"""
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LINUX_SONAMES

for soname in SUPPORTED_LINUX_SONAMES.get(libname, ()):
for soname in get_candidate_sonames(libname):
try:
handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD)
except OSError:
Expand All @@ -96,9 +116,7 @@ def load_with_system_search(libname: str) -> Optional[LoadedDL]:
Raises:
RuntimeError: If the library is loaded but no expected symbol is found
"""
candidate_sonames = list(SUPPORTED_LINUX_SONAMES.get(libname, ()))
candidate_sonames.append(f"lib{libname}.so")
for soname in candidate_sonames:
for soname in get_candidate_sonames(libname):
try:
handle = ctypes.CDLL(soname, CDLL_MODE)
abs_path = abs_path_for_dynamic_library(libname, handle)
Expand Down
11 changes: 4 additions & 7 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from typing import Optional

from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY,
SUPPORTED_WINDOWS_DLLS,
)

# Mirrors WinBase.h (unfortunately not defined already elsewhere)
WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100
Expand Down Expand Up @@ -110,7 +114,6 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
>>> if loaded is not None:
... print(f"Library already loaded from {loaded.abs_path}")
"""
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS

for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()):
handle = kernel32.GetModuleHandleW(dll_name)
Expand All @@ -129,8 +132,6 @@ def load_with_system_search(libname: str) -> Optional[LoadedDL]:
Returns:
A LoadedDL object if successful, None if the library cannot be loaded
"""
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS

for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()):
handle = kernel32.LoadLibraryExW(dll_name, None, 0)
if handle:
Expand All @@ -153,10 +154,6 @@ def load_with_abs_path(libname: str, found_path: str) -> LoadedDL:
Raises:
RuntimeError: If the DLL cannot be loaded
"""
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY,
)

if libname in LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY:
add_dll_directory(found_path)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# SUPPORTED_LIBNAMES
# SUPPORTED_WINDOWS_DLLS
# SUPPORTED_LINUX_SONAMES
# EXPECTED_LIB_SYMBOLS

import sys

Expand Down Expand Up @@ -401,39 +400,3 @@ def is_suppressed_dll_file(path_basename: str) -> bool:
# nvrtc64_120_0.dll
return path_basename.endswith(".alt.dll") or "-builtins" in path_basename
return path_basename.startswith(("cudart32_", "nvvm32"))


# Based on `nm -D --defined-only` output for Linux x86_64 distributions.
EXPECTED_LIB_SYMBOLS = {
"nvJitLink": (
"__nvJitLinkCreate_12_0", # 12.0 through 12.9
"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_Ctx",),
"nppicc": ("nppiColorToGray_8u_C3C1R_Ctx",),
"nppidei": ("nppiCopy_8u_C1R_Ctx",),
"nppif": ("nppiFilterSobelHorizBorder_8u_C1R_Ctx",),
"nppig": ("nppiResize_8u_C1R_Ctx",),
"nppim": ("nppiErode_8u_C1R_Ctx",),
"nppist": ("nppiMean_8u_C1R_Ctx",),
"nppisu": ("nppiFree",),
"nppitc": ("nppiThreshold_8u_C1R_Ctx",),
"npps": ("nppsAdd_32f_Ctx",),
"nvblas": ("dgemm",),
"cufile": ("cuFileGetVersion",),
# "cufile_rdma": ("rdma_buffer_reg",),
"nvjpeg": ("nvjpegCreate",),
}
2 changes: 1 addition & 1 deletion cuda_pathfinder/cuda/pathfinder/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

__version__ = "1.1.1a0"
__version__ = "1.1.1a1"
14 changes: 8 additions & 6 deletions cuda_pathfinder/tests/test_load_nvidia_dynamic_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ def test_supported_libnames_windows_libnames_requiring_os_add_dll_directory_cons
)


def test_supported_libnames_all_expected_lib_symbols_consistency():
assert tuple(sorted(supported_nvidia_libs.SUPPORTED_LIBNAMES_ALL)) == tuple(
sorted(supported_nvidia_libs.EXPECTED_LIB_SYMBOLS.keys())
)


def test_runtime_error_on_non_64bit_python():
with (
patch("struct.calcsize", return_value=3), # fake 24-bit pointer
Expand All @@ -68,6 +62,12 @@ def build_child_process_failed_for_libname_message(libname, result):
)


def validate_abs_path(abs_path):
assert abs_path, f"empty path: {abs_path=!r}"
assert os.path.isabs(abs_path), f"not absolute: {abs_path=!r}"
assert os.path.isfile(abs_path), f"not a file: {abs_path=!r}"


def child_process_func(libname):
import os

Expand All @@ -76,6 +76,7 @@ def child_process_func(libname):
loaded_dl_fresh = load_nvidia_dynamic_lib(libname)
if loaded_dl_fresh.was_already_loaded_from_elsewhere:
raise RuntimeError("loaded_dl_fresh.was_already_loaded_from_elsewhere")
validate_abs_path(loaded_dl_fresh.abs_path)

loaded_dl_from_cache = load_nvidia_dynamic_lib(libname)
if loaded_dl_from_cache is not loaded_dl_fresh:
Expand All @@ -86,6 +87,7 @@ def child_process_func(libname):
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})")
validate_abs_path(loaded_dl_no_cache.abs_path)

sys.stdout.write(f"{loaded_dl_fresh.abs_path!r}\n")

Expand Down
Loading