Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
import os

# Site-packages bin directories where binaries might be found
# Site-packages bin directories where binaries might be found, in search order.
# Based on NVIDIA wheel layouts (same for Linux and Windows)
_CUDA_NVCC_BIN = os.path.join("nvidia", "cuda_nvcc", "bin")
_CUDA13_BIN = os.path.join("nvidia", "cu13", "bin")
Expand Down
140 changes: 71 additions & 69 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,3 @@

#: Canonical registry of all known libraries.
LIB_DESCRIPTORS: dict[str, LibDescriptor] = {desc.name: desc for desc in DESCRIPTOR_CATALOG}


def linux_soname_candidates(desc: LibDescriptor) -> tuple[str, ...]:
"""Return declared Linux SONAMEs in runtime preference order."""
# The catalog is authored oldest -> newest; loading prefers newest -> oldest.
return tuple(reversed(desc.linux_sonames))
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import sys
from typing import TYPE_CHECKING, cast

from cuda.pathfinder._dynamic_libs.lib_descriptor import linux_soname_candidates
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL

if TYPE_CHECKING:
Expand Down Expand Up @@ -133,7 +132,7 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
if sys.platform == "linux":

def check_if_already_loaded_from_elsewhere(desc: LibDescriptor) -> LoadedDL | None:
for soname in linux_soname_candidates(desc):
for soname in desc.linux_sonames:
try:
handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD)
except OSError:
Expand Down Expand Up @@ -171,7 +170,7 @@ def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None:
A LoadedDL object if successful, None if the library cannot be loaded

"""
for soname in linux_soname_candidates(desc):
for soname in desc.linux_sonames:
try:
handle = _load_lib(desc, soname)
except OSError:
Expand Down
10 changes: 2 additions & 8 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import struct
import sys
import warnings
from collections.abc import Iterator
from typing import TYPE_CHECKING

from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
Expand Down Expand Up @@ -121,13 +120,8 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.wintypes.HMODULE)
return buffer.value


def _candidate_dll_names(desc: LibDescriptor) -> Iterator[str]:
"""Yield tabulated DLL names from newest to oldest."""
return reversed(desc.windows_dlls)


def check_if_already_loaded_from_elsewhere(desc: LibDescriptor) -> LoadedDL | None:
for dll_name in _candidate_dll_names(desc):
for dll_name in desc.windows_dlls:
handle = kernel32.GetModuleHandleW(dll_name)
if handle:
abs_path = abs_path_for_dynamic_library(desc.name, handle)
Expand All @@ -154,7 +148,7 @@ def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None:
Returns:
A LoadedDL object if successful, None if the library cannot be loaded
"""
for dll_name in _candidate_dll_names(desc):
for dll_name in desc.windows_dlls:
handle = kernel32.LoadLibraryExW(dll_name, None, 0)
if handle:
abs_path = abs_path_for_dynamic_library(desc.name, handle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pathlib import PurePath
from typing import Protocol, cast

from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor, linux_soname_candidates
from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import is_suppressed_dll_file
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
Expand All @@ -34,7 +34,7 @@ def _no_such_file_in_sub_dirs(


def _find_descriptor_so_under_dir(dirpath: str, desc: LibDescriptor) -> str | None:
for soname in linux_soname_candidates(desc):
for soname in desc.linux_sonames:
path = os.path.join(dirpath, soname)
if os.path.isfile(path):
return path
Expand Down Expand Up @@ -73,8 +73,7 @@ def candidate_is_usable(path: str) -> bool:
return False
return target_arch is None or windows_pe_matches_arch(path, target_arch)

# Try the descriptor's known DLL names in its established search order.
for dll_basename in reversed(cast(tuple[str, ...], desc.windows_dlls)):
for dll_basename in desc.windows_dlls:
path = os.path.join(dirpath, dll_basename)
if candidate_is_usable(path):
return path
Expand Down Expand Up @@ -138,7 +137,7 @@ def find_in_lib_dir(
@dataclass(frozen=True, slots=True)
class LinuxSearchPlatform:
def lib_searched_for(self, desc: LibDescriptor) -> str:
return " or ".join(linux_soname_candidates(desc))
return " or ".join(desc.linux_sonames)

def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
return cast(tuple[str, ...], desc.site_packages_linux)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
_DRIVER_DESCRIPTORS = tuple(desc for desc in DESCRIPTOR_CATALOG if desc.packaged_with == "driver")
_NON_CTK_DESCRIPTORS = _OTHER_DESCRIPTORS + _DRIVER_DESCRIPTORS


def _legacy_least_preferred_first(names: tuple[str, ...]) -> tuple[str, ...]:
"""Preserve the historical ordering of legacy filename projections."""
return tuple(reversed(names))


SUPPORTED_LIBNAMES_COMMON = tuple(desc.name for desc in _CTK_DESCRIPTORS if desc.linux_sonames and desc.windows_dlls)
SUPPORTED_LIBNAMES_LINUX_ONLY = tuple(
desc.name for desc in _CTK_DESCRIPTORS if desc.linux_sonames and not desc.windows_dlls
Expand Down Expand Up @@ -60,14 +66,26 @@
DIRECT_DEPENDENCIES_CTK = {desc.name: desc.dependencies for desc in _CTK_DESCRIPTORS if desc.dependencies}
DIRECT_DEPENDENCIES = {desc.name: desc.dependencies for desc in DESCRIPTOR_CATALOG if desc.dependencies}

SUPPORTED_LINUX_SONAMES_CTK = {desc.name: desc.linux_sonames for desc in _CTK_DESCRIPTORS if desc.linux_sonames}
SUPPORTED_LINUX_SONAMES_OTHER = {desc.name: desc.linux_sonames for desc in _OTHER_DESCRIPTORS if desc.linux_sonames}
SUPPORTED_LINUX_SONAMES_DRIVER = {desc.name: desc.linux_sonames for desc in _DRIVER_DESCRIPTORS if desc.linux_sonames}
SUPPORTED_LINUX_SONAMES_CTK = {
desc.name: _legacy_least_preferred_first(desc.linux_sonames) for desc in _CTK_DESCRIPTORS if desc.linux_sonames
}
Comment on lines +69 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the order of libnames are actually non-essential here, is that correct? Not a change request, just confirming my thoughts. I see that the only place uses the global variable is here and this test. And neither depend on the order, actually, they use set-like operation at either places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct. There are a few additional in-tree uses of these mappings, but they are all order-insensitive: they use dictionary keys, membership, parametrization, or uniqueness checks. Runtime discovery and loading consume the descriptor tuples directly.

The reversal here is deliberately conservative. supported_nvidia_libs.py is retained for legacy compatibility, and although these constants are private implementation details, client code could have imported and iterated, indexed, or compared their tuple values. Preserving their historical order costs only this small conversion and keeps the refactor mechanically strictly behavior-neutral.

SUPPORTED_LINUX_SONAMES_OTHER = {
desc.name: _legacy_least_preferred_first(desc.linux_sonames) for desc in _OTHER_DESCRIPTORS if desc.linux_sonames
}
SUPPORTED_LINUX_SONAMES_DRIVER = {
desc.name: _legacy_least_preferred_first(desc.linux_sonames) for desc in _DRIVER_DESCRIPTORS if desc.linux_sonames
}
SUPPORTED_LINUX_SONAMES = SUPPORTED_LINUX_SONAMES_CTK | SUPPORTED_LINUX_SONAMES_OTHER | SUPPORTED_LINUX_SONAMES_DRIVER

SUPPORTED_WINDOWS_DLLS_CTK = {desc.name: desc.windows_dlls for desc in _CTK_DESCRIPTORS if desc.windows_dlls}
SUPPORTED_WINDOWS_DLLS_OTHER = {desc.name: desc.windows_dlls for desc in _OTHER_DESCRIPTORS if desc.windows_dlls}
SUPPORTED_WINDOWS_DLLS_DRIVER = {desc.name: desc.windows_dlls for desc in _DRIVER_DESCRIPTORS if desc.windows_dlls}
SUPPORTED_WINDOWS_DLLS_CTK = {
desc.name: _legacy_least_preferred_first(desc.windows_dlls) for desc in _CTK_DESCRIPTORS if desc.windows_dlls
}
SUPPORTED_WINDOWS_DLLS_OTHER = {
desc.name: _legacy_least_preferred_first(desc.windows_dlls) for desc in _OTHER_DESCRIPTORS if desc.windows_dlls
}
SUPPORTED_WINDOWS_DLLS_DRIVER = {
desc.name: _legacy_least_preferred_first(desc.windows_dlls) for desc in _DRIVER_DESCRIPTORS if desc.windows_dlls
}
SUPPORTED_WINDOWS_DLLS = SUPPORTED_WINDOWS_DLLS_CTK | SUPPORTED_WINDOWS_DLLS_OTHER | SUPPORTED_WINDOWS_DLLS_DRIVER

LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY = tuple(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

@dataclass(frozen=True, slots=True)
class HeaderDescriptorSpec:
"""Header metadata with ordered alternatives searched first-to-last."""

name: str
packaged_with: HeaderPackagedWith
header_basename: str
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class LocatedBitcodeLib:


class _BitcodeLibInfo(TypedDict):
"""Bitcode-library metadata with ordered alternatives searched first-to-last."""

filename: str
rel_path: str
site_packages_dirs: tuple[str, ...]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class LocatedStaticLib:


class _StaticLibInfo(TypedDict):
"""Static-library metadata with ordered alternatives searched first-to-last."""

filename: str
ctk_rel_paths: tuple[str, ...]
conda_rel_paths: tuple[str, ...]
Expand Down
22 changes: 16 additions & 6 deletions cuda_pathfinder/docs/source/release/1.8.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,20 @@ Bugfixes
(`PR #2689 <https://github.com/NVIDIA/cuda-python/pull/2689>`_)

* Make Linux filesystem discovery, already-loaded detection, and native loading
use one descriptor-declared, newest-first library filename list. Remove the
implicit ``lib<name>.so`` and broad ``.so*`` filesystem fallbacks, so
explicit directory layouts must expose a declared filename. Explicitly
declare the unversioned ``libnvvm.so`` wheel filename to preserve CUDA 12.9
wheel discovery, and correct the ``cufftMp`` catalog order so ABI 12 is
preferred over ABI 11.
use one descriptor-declared library filename list in runtime preference
order. Remove the implicit ``lib<name>.so`` and broad ``.so*`` filesystem
fallbacks, so explicit directory layouts must expose a declared filename.
Explicitly declare the unversioned ``libnvvm.so`` wheel filename to preserve
CUDA 12.9 wheel discovery, and correct the ``cufftMp`` catalog order so ABI
12 is preferred over ABI 11.
(`PR #2689 <https://github.com/NVIDIA/cuda-python/pull/2689>`_)

Internal maintenance
--------------------

* Author dynamic-library filename candidates directly in runtime preference
order and consume them without catalog-to-runtime reversals. Document the
same first-to-last candidate convention for dynamic libraries, headers,
binary utilities, static libraries, and bitcode libraries. Runtime selection
is unchanged.
(`PR #2708 <https://github.com/NVIDIA/cuda-python/pull/2708>`_)
6 changes: 3 additions & 3 deletions cuda_pathfinder/tests/test_descriptor_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import pytest

from cuda.pathfinder._dynamic_libs.descriptor_catalog import DESCRIPTOR_CATALOG, DescriptorSpec, WindowsSearchDirs
from cuda.pathfinder._utils.path_sort import numeric_aware_path_sort_key

_VALID_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_VALID_PACKAGED_WITH_VALUES = {"ctk", "other", "driver"}
Expand Down Expand Up @@ -92,8 +91,9 @@ def test_linux_sonames_look_like_sonames(spec: DescriptorSpec):

@pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name)
@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_linux_sonames_are_ordered_oldest_to_newest(spec: DescriptorSpec):
assert spec.linux_sonames == tuple(sorted(spec.linux_sonames, key=numeric_aware_path_sort_key))
def test_library_filenames_are_unique_per_platform(spec: DescriptorSpec):
assert len(spec.linux_sonames) == len(set(spec.linux_sonames))
assert len(spec.windows_dlls) == len({dll.casefold() for dll in spec.windows_dlls})


@pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name)
Expand Down
37 changes: 24 additions & 13 deletions cuda_pathfinder/tests/test_lib_descriptor.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Tests verifying that the LibDescriptor registry faithfully represents
the existing data tables in supported_nvidia_libs.py."""
"""Tests for the canonical library descriptors and their legacy projections."""

import pytest

from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS, linux_soname_candidates
from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
DIRECT_DEPENDENCIES,
LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY,
Expand Down Expand Up @@ -54,12 +53,12 @@ def test_registry_has_no_extra_entries():

@pytest.mark.parametrize("name", sorted(LIB_DESCRIPTORS))
def test_linux_sonames_match(name):
assert LIB_DESCRIPTORS[name].linux_sonames == SUPPORTED_LINUX_SONAMES.get(name, ())
assert tuple(reversed(LIB_DESCRIPTORS[name].linux_sonames)) == SUPPORTED_LINUX_SONAMES.get(name, ())


@pytest.mark.parametrize("name", sorted(LIB_DESCRIPTORS))
def test_windows_dlls_match(name):
assert LIB_DESCRIPTORS[name].windows_dlls == SUPPORTED_WINDOWS_DLLS.get(name, ())
assert tuple(reversed(LIB_DESCRIPTORS[name].windows_dlls)) == SUPPORTED_WINDOWS_DLLS.get(name, ())


@pytest.mark.parametrize("name", sorted(LIB_DESCRIPTORS))
Expand Down Expand Up @@ -156,23 +155,35 @@ def test_descriptor_is_frozen():


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_linux_soname_candidates_are_declared_names_newest_first():
def test_linux_sonames_are_authored_in_runtime_preference_order():
desc = LIB_DESCRIPTORS["cudart"]

assert desc.linux_sonames == ("libcudart.so.12", "libcudart.so.13")
assert linux_soname_candidates(desc) == ("libcudart.so.13", "libcudart.so.12")
assert desc.linux_sonames == ("libcudart.so.13", "libcudart.so.12")


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_linux_soname_candidates_preserve_explicit_unversioned_name():
def test_linux_sonames_preserve_explicit_unversioned_name():
desc = LIB_DESCRIPTORS["nvcudla"]

assert linux_soname_candidates(desc) == ("libnvcudla.so",)
assert desc.linux_sonames == ("libnvcudla.so",)


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_linux_soname_candidates_prefer_versioned_name_over_declared_unversioned_name():
def test_linux_sonames_prefer_versioned_name_over_declared_unversioned_name():
desc = LIB_DESCRIPTORS["nvvm"]

assert desc.linux_sonames == ("libnvvm.so", "libnvvm.so.4")
assert linux_soname_candidates(desc) == ("libnvvm.so.4", "libnvvm.so")
assert desc.linux_sonames == ("libnvvm.so.4", "libnvvm.so")


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_windows_dlls_are_authored_in_runtime_preference_order():
desc = LIB_DESCRIPTORS["nvvm"]

assert desc.windows_dlls == ("nvvm70.dll", "nvvm64_40_0.dll", "nvvm64.dll")


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_cufft_mp_sonames_preserve_abi_preference():
desc = LIB_DESCRIPTORS["cufftMp"]

assert desc.linux_sonames == ("libcufftMp.so.12", "libcufftMp.so.11")
6 changes: 3 additions & 3 deletions cuda_pathfinder/tests/test_load_dl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ def _descriptor() -> DescriptorSpec:
return DescriptorSpec(
name="probe",
packaged_with="other",
linux_sonames=("libprobe.so.12", "libprobe.so.13"),
linux_sonames=("libprobe.so.13", "libprobe.so.12"),
)


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_already_loaded_library_checks_only_declared_sonames_newest_first(mocker):
def test_already_loaded_library_checks_declared_sonames_in_preference_order(mocker):
queried_sonames: list[tuple[str, int]] = []

def cdll(soname, mode):
Expand All @@ -41,7 +41,7 @@ def cdll(soname, mode):


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_system_search_checks_only_declared_sonames_newest_first(mocker):
def test_system_search_checks_declared_sonames_in_preference_order(mocker):
queried_sonames: list[str] = []

def load_lib(_desc, soname):
Expand Down
31 changes: 24 additions & 7 deletions cuda_pathfinder/tests/test_load_dl_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,45 @@


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_already_loaded_library_checks_known_dlls_newest_first(mocker):
def test_already_loaded_library_checks_known_dlls_in_preference_order(mocker):
desc = LIB_DESCRIPTORS["cublasLt"]
oldest_dll = desc.windows_dlls[0]
newest_dll = desc.windows_dlls[-1]
preferred_dll = desc.windows_dlls[0]
fallback_dll = desc.windows_dlls[-1]
queried_dlls: list[str] = []
handle = 0xBEEF

def get_module_handle(dll_name):
queried_dlls.append(dll_name)
return handle if dll_name == oldest_dll else 0
return handle if dll_name == fallback_dll else 0

mocker.patch.object(load_dl_windows.kernel32, "GetModuleHandleW", side_effect=get_module_handle)
mocker.patch.object(
load_dl_windows,
"abs_path_for_dynamic_library",
return_value=rf"C:\CUDA\bin\{oldest_dll}",
return_value=rf"C:\CUDA\bin\{fallback_dll}",
)

loaded = load_dl_windows.check_if_already_loaded_from_elsewhere(desc)

assert loaded is not None
assert queried_dlls == [newest_dll, oldest_dll]
assert queried_dlls == list(desc.windows_dlls)


@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_system_search_checks_known_dlls_in_preference_order(mocker):
desc = LIB_DESCRIPTORS["cublasLt"]
queried_dlls: list[str] = []

def load_library(dll_name, _file, _flags):
queried_dlls.append(dll_name)
return 0

mocker.patch.object(load_dl_windows.kernel32, "LoadLibraryExW", side_effect=load_library)

loaded = load_dl_windows.load_with_system_search(desc)

assert loaded is None
assert queried_dlls == list(desc.windows_dlls)


@pytest.mark.parametrize(
Expand All @@ -46,7 +63,7 @@ def test_already_loaded_library_registers_resolved_directory_by_descriptor_polic
mocker, tmp_path, libname, register_directory
):
desc = LIB_DESCRIPTORS[libname]
resolved_path = str(tmp_path / desc.windows_dlls[-1])
resolved_path = str(tmp_path / desc.windows_dlls[0])
handle = 0xBEEF
mocker.patch.object(load_dl_windows.kernel32, "GetModuleHandleW", return_value=handle)
mocker.patch.object(load_dl_windows, "abs_path_for_dynamic_library", return_value=resolved_path)
Expand Down
Loading
Loading