From 7c3426fdcb507ff1ea2c08f9993fe163bdb8ef38 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Aug 2026 12:33:34 -0700 Subject: [PATCH 1/4] Use exact Windows DLL names for CUPTI Restrict CUPTI filesystem discovery to descriptor-declared DLL names and remove the now-unused fallback-glob metadata and search machinery. This keeps filesystem discovery consistent with already-loaded detection and native loading, avoiding undeclared wildcard matches. --- .../_dynamic_libs/descriptor_catalog.py | 2 -- .../_dynamic_libs/search_platform.py | 20 +++-------- .../tests/test_descriptor_catalog.py | 34 ------------------- cuda_pathfinder/tests/test_search_steps.py | 30 +++------------- 4 files changed, 9 insertions(+), 77 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py index d745b2bd68c..aaa9074d423 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py @@ -61,7 +61,6 @@ class DescriptorSpec: packaged_with: PackagedWith linux_sonames: tuple[str, ...] = () windows_dlls: tuple[str, ...] = () - windows_dll_fallback_globs: tuple[str, ...] = () supported_windows_arch: tuple[WindowsArch, ...] = () site_packages_linux: tuple[str, ...] = () site_packages_windows: WindowsSearchDirs = WindowsSearchDirs() @@ -377,7 +376,6 @@ class DescriptorSpec: "cupti64_2026.2.1.dll", "cupti64_2026.3.0.dll", ), - windows_dll_fallback_globs=("cupti64_*.dll",), supported_windows_arch=("x64", "arm64"), site_packages_linux=("nvidia/cu13/lib", "nvidia/cuda_cupti/lib"), site_packages_windows=_ctk_windows_wheel_dirs("nvidia/cu13/bin", "nvidia/cuda_cupti/bin"), diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py index 7b284714710..ca2ccbcc138 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py @@ -20,7 +20,6 @@ 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.path_sort import numeric_aware_path_sort_key from cuda.pathfinder._utils.platform_aware import IS_WINDOWS from cuda.pathfinder._utils.windows_arch import windows_pe_matches_arch, windows_python_arch @@ -76,19 +75,11 @@ def candidate_is_usable(path: str) -> bool: return False return target_arch is None or windows_pe_matches_arch(path, target_arch) - # Prefer the descriptor's known DLL names in its established search order. - # Explicit globs provide a collision-safe forward-compatible fallback for - # libraries whose full version is encoded in the filename (for example CUPTI). + # Try the descriptor's known DLL names in its established search order. for dll_basename in reversed(cast(tuple[str, ...], desc.windows_dlls)): path = os.path.join(dirpath, dll_basename) if candidate_is_usable(path): return path - - for dll_glob in desc.windows_dll_fallback_globs: - file_wild = os.path.join(dirpath, dll_glob) - for path in sorted(glob.glob(file_wild), key=numeric_aware_path_sort_key, reverse=True): - if candidate_is_usable(path): - return path return None @@ -218,7 +209,7 @@ class WindowsSearchPlatform: target_arch: str def lib_searched_for(self, libname: str) -> str: - return f"{libname}*.dll" + return f"known {libname} DLL" def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.site_packages_windows.for_arch(self.target_arch)) @@ -267,19 +258,18 @@ def find_in_lib_dir( self, lib_dir: str, desc: LibDescriptor, - _lib_searched_for: str, + lib_searched_for: str, error_messages: list[str], attachments: list[str], ) -> str | None: - file_wild = desc.name + "*.dll" target_arch = self.target_arch if desc.requires_windows_binary_arch_check else None dll_name = _find_descriptor_dll_under_dir(lib_dir, desc, target_arch) if dll_name is not None: return dll_name if target_arch is None: - error_messages.append(f"No such file: {file_wild}") + error_messages.append(f"No such file: {lib_searched_for}") else: - error_messages.append(f"No {target_arch}-compatible PE file: {file_wild}") + error_messages.append(f"No {target_arch}-compatible PE file: {lib_searched_for}") attachments.append(f' listdir("{lib_dir}"):') if not os.path.isdir(lib_dir): attachments.append(" DIRECTORY DOES NOT EXIST") diff --git a/cuda_pathfinder/tests/test_descriptor_catalog.py b/cuda_pathfinder/tests/test_descriptor_catalog.py index e4a6a609c6c..e7f68a5aceb 100644 --- a/cuda_pathfinder/tests/test_descriptor_catalog.py +++ b/cuda_pathfinder/tests/test_descriptor_catalog.py @@ -9,7 +9,6 @@ from __future__ import annotations -import fnmatch import re import pytest @@ -96,38 +95,6 @@ def test_windows_dlls_look_like_dlls(spec: DescriptorSpec): assert dll.endswith(".dll"), f"Unexpected Windows DLL format: {dll}" -@pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name) -@pytest.mark.agent_authored(model="gpt-5.6-sol") -def test_windows_dll_fallback_globs_are_basenames(spec: DescriptorSpec): - for dll_glob in spec.windows_dll_fallback_globs: - assert "*" in dll_glob - assert dll_glob.endswith(".dll") - assert "/" not in dll_glob - assert "\\" not in dll_glob - - -@pytest.mark.agent_authored(model="gpt-5.6-sol") -def test_only_cupti_uses_forward_compatible_windows_dll_glob(): - specs_with_globs = {spec.name for spec in DESCRIPTOR_CATALOG if spec.windows_dll_fallback_globs} - - assert specs_with_globs == {"cupti"} - assert _CATALOG_BY_NAME["cupti"].windows_dll_fallback_globs == ("cupti64_*.dll",) - - -@pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name) -@pytest.mark.agent_authored(model="gpt-5.6-sol") -def test_windows_dll_fallback_globs_do_not_match_other_descriptors(spec: DescriptorSpec): - for dll_glob in spec.windows_dll_fallback_globs: - sibling_matches = { - (other.name, dll) - for other in DESCRIPTOR_CATALOG - if other is not spec - for dll in other.windows_dlls - if fnmatch.fnmatchcase(dll.casefold(), dll_glob.casefold()) - } - assert not sibling_matches, f"{spec.name} glob {dll_glob!r} also matches {sibling_matches}" - - @pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name) @pytest.mark.agent_authored(model="gpt-5") def test_supported_windows_arch_is_explicit_and_canonical(spec: DescriptorSpec): @@ -188,7 +155,6 @@ def test_cudnn_metadata_matches_supported_layouts(): assert spec.packaged_with == "other" assert spec.linux_sonames == ("libcudnn.so.9",) assert spec.windows_dlls == ("cudnn64_9.dll",) - assert not spec.windows_dll_fallback_globs assert spec.supported_windows_arch == ("x64", "arm64") assert spec.site_packages_linux == ("nvidia/cudnn/lib",) assert spec.site_packages_windows == WindowsSearchDirs.x64_only("nvidia/cudnn/bin") diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 9e0858acecb..dfbd66b1048 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -100,7 +100,7 @@ def test_lib_searched_for_linux(self): def test_lib_searched_for_windows(self): ctx = SearchContext(_make_desc(name="cublas"), platform=WindowsSearchPlatform(target_arch="x64")) - assert ctx.lib_searched_for == "cublas*.dll" + assert ctx.lib_searched_for == "known cublas DLL" def test_raise_not_found_includes_messages(self): ctx = _ctx() @@ -331,43 +331,21 @@ def test_windows_exact_names_reject_cudnn_sidecar(self, mocker, tmp_path): assert result is None @pytest.mark.agent_authored(model="gpt-5.6-sol") - def test_windows_explicit_glob_finds_unlisted_future_cupti(self, mocker, tmp_path): + def test_windows_matching_rejects_unlisted_cupti_name(self, mocker, tmp_path): bin_dir = tmp_path / "nvidia" / "cuda_cupti" / "bin" bin_dir.mkdir(parents=True) - dll = bin_dir / "cupti64_2027.1.0.dll" - dll.touch() - - mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", - return_value=[str(bin_dir)], - ) - mocker.patch(f"{_PLAT_MOD}.is_suppressed_dll_file", return_value=False) - - result = find_in_site_packages( - _ctx(LIB_DESCRIPTORS["cupti"], platform=WindowsSearchPlatform(target_arch="x64")) - ) - - assert result == FindResult(str(dll), "site-packages") - - @pytest.mark.agent_authored(model="gpt-5.6-sol") - def test_windows_explicit_glob_prefers_known_cupti_name(self, mocker, tmp_path): - bin_dir = tmp_path / "nvidia" / "cuda_cupti" / "bin" - bin_dir.mkdir(parents=True) - known_dll = bin_dir / "cupti64_2026.3.0.dll" - known_dll.touch() - (bin_dir / "cupti64_2027.1.0.dll").touch() + (bin_dir / "cupti64_14.dll").touch() mocker.patch( f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", return_value=[str(bin_dir)], ) - mocker.patch(f"{_PLAT_MOD}.is_suppressed_dll_file", return_value=False) result = find_in_site_packages( _ctx(LIB_DESCRIPTORS["cupti"], platform=WindowsSearchPlatform(target_arch="x64")) ) - assert result == FindResult(str(known_dll), "site-packages") + assert result is None @pytest.mark.agent_authored(model="gpt-5.6-sol") def test_windows_known_cupti_names_prefer_newest(self, tmp_path): From f20ad663d403e40ba34cf644c7a719dbb1041108 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Aug 2026 15:13:30 -0700 Subject: [PATCH 2/4] Use exact Linux SONAMEs for library discovery Use one descriptor-defined newest-first candidate list for filesystem discovery, RTLD_NOLOAD checks, and native loading. Remove the generic unversioned and glob fallbacks so undeclared ABI names cannot be found on disk while remaining invisible to resident checks. Correct the cufftMp catalog ordering so ABI 12 is preferred over ABI 11. Layouts must provide an exact declared SONAME alias; physical patch filenames without that alias are intentionally not discovered. --- .../_dynamic_libs/descriptor_catalog.py | 3 +- .../_dynamic_libs/lib_descriptor.py | 6 ++ .../pathfinder/_dynamic_libs/load_dl_linux.py | 12 +-- .../_dynamic_libs/search_platform.py | 82 +++++++----------- .../pathfinder/_dynamic_libs/search_steps.py | 4 +- .../tests/test_ctk_root_discovery.py | 4 +- .../tests/test_descriptor_catalog.py | 7 ++ cuda_pathfinder/tests/test_lib_descriptor.py | 17 +++- cuda_pathfinder/tests/test_load_dl_linux.py | 56 ++++++++++++ cuda_pathfinder/tests/test_search_steps.py | 85 ++++++++----------- 10 files changed, 162 insertions(+), 114 deletions(-) create mode 100644 cuda_pathfinder/tests/test_load_dl_linux.py diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py index aaa9074d423..54c5d1b770a 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py @@ -59,6 +59,7 @@ def _ctk_windows_wheel_dirs(cuda13_bin_dir: str, cuda12_dir: str) -> WindowsSear class DescriptorSpec: name: str packaged_with: PackagedWith + # Keep declared SONAMEs in oldest -> newest order. linux_sonames: tuple[str, ...] = () windows_dlls: tuple[str, ...] = () supported_windows_arch: tuple[WindowsArch, ...] = () @@ -412,7 +413,7 @@ class DescriptorSpec: DescriptorSpec( name="cufftMp", packaged_with="other", - linux_sonames=("libcufftMp.so.12", "libcufftMp.so.11"), + linux_sonames=("libcufftMp.so.11", "libcufftMp.so.12"), site_packages_linux=("nvidia/cufftmp/cu13/lib", "nvidia/cufftmp/cu12/lib"), dependencies=("nvshmem_host",), requires_rtld_deepbind=True, diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/lib_descriptor.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/lib_descriptor.py index f5e643e28fa..433f636c10e 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/lib_descriptor.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/lib_descriptor.py @@ -22,3 +22,9 @@ #: 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)) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py index 684cb9e83e5..429d37b5f62 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py @@ -10,6 +10,7 @@ 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: @@ -129,17 +130,10 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str: return os.path.join(l_origin, os.path.basename(l_name)) -def _candidate_sonames(desc: LibDescriptor) -> list[str]: - # Reverse tabulated names to achieve new -> old search order. - candidates = list(reversed(desc.linux_sonames)) - candidates.append(f"lib{desc.name}.so") - return candidates - - if sys.platform == "linux": def check_if_already_loaded_from_elsewhere(desc: LibDescriptor) -> LoadedDL | None: - for soname in _candidate_sonames(desc): + for soname in linux_soname_candidates(desc): try: handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD) except OSError: @@ -177,7 +171,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 _candidate_sonames(desc): + for soname in linux_soname_candidates(desc): try: handle = _load_lib(desc, soname) except OSError: diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py index ca2ccbcc138..a9afb545409 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Platform abstraction for filesystem search steps. @@ -10,14 +10,13 @@ from __future__ import annotations -import glob import os from collections.abc import Sequence from dataclasses import dataclass from pathlib import PurePath from typing import Protocol, cast -from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor +from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor, linux_soname_candidates 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 @@ -25,41 +24,40 @@ def _no_such_file_in_sub_dirs( - sub_dirs: Sequence[str], file_wild: str, error_messages: list[str], attachments: list[str] + sub_dirs: Sequence[str], file_description: str, error_messages: list[str], attachments: list[str] ) -> None: - error_messages.append(f"No such file: {file_wild}") + error_messages.append(f"No such file: {file_description}") 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_descriptor_so_under_dir(dirpath: str, desc: LibDescriptor) -> str | None: + for soname in linux_soname_candidates(desc): + path = os.path.join(dirpath, soname) + if os.path.isfile(path): + return path + return None + + def _find_so_in_rel_dirs( rel_dirs: tuple[str, ...], - so_basename: str, + desc: LibDescriptor, + file_description: str, error_messages: list[str], attachments: list[str], ) -> str | None: sub_dirs_searched: list[tuple[str, ...]] = [] - file_wild = so_basename + "*" for rel_dir in rel_dirs: sub_dir = PurePath(rel_dir).parts for abs_dir in find_sub_dirs_all_sitepackages(sub_dir): - # Exact unversioned match first; fall back to versioned names because some - # distros only ship lib.so. (e.g. conda libcupti). Only one match - # is expected in practice. Sort in reverse so the newest-sorting name wins if - # multiple coexist, matching the newest-first bias elsewhere in pathfinder - # (see LinuxSearchPlatform.find_in_lib_dir and load_dl_linux._candidate_sonames). - # Issue #1732 tracks the deferred question of raising on true ambiguity. - so_name = os.path.join(abs_dir, so_basename) - if os.path.isfile(so_name): - return so_name - for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild)), reverse=True): - if os.path.isfile(so_name): - return so_name + so_path = _find_descriptor_so_under_dir(abs_dir, desc) + if so_path is not None: + return so_path sub_dirs_searched.append(sub_dir) for sub_dir in sub_dirs_searched: - _no_such_file_in_sub_dirs(sub_dir, file_wild, error_messages, attachments) + _no_such_file_in_sub_dirs(sub_dir, file_description, error_messages, attachments) return None @@ -106,7 +104,7 @@ def _find_dll_in_rel_dirs( class SearchPlatform(Protocol): - def lib_searched_for(self, libname: str) -> str: ... + def lib_searched_for(self, desc: LibDescriptor) -> str: ... def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: ... @@ -124,7 +122,6 @@ def find_in_site_packages( self, rel_dirs: tuple[str, ...], desc: LibDescriptor, - lib_searched_for: str, error_messages: list[str], attachments: list[str], ) -> str | None: ... @@ -133,7 +130,6 @@ def find_in_lib_dir( self, lib_dir: str, desc: LibDescriptor, - lib_searched_for: str, error_messages: list[str], attachments: list[str], ) -> str | None: ... @@ -141,8 +137,8 @@ def find_in_lib_dir( @dataclass(frozen=True, slots=True) class LinuxSearchPlatform: - def lib_searched_for(self, libname: str) -> str: - return f"lib{libname}.so" + def lib_searched_for(self, desc: LibDescriptor) -> str: + return " or ".join(linux_soname_candidates(desc)) def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.site_packages_linux) @@ -165,36 +161,23 @@ def program_files_root_globs(self, _desc: LibDescriptor) -> tuple[str, ...]: def find_in_site_packages( self, rel_dirs: tuple[str, ...], - _desc: LibDescriptor, - lib_searched_for: str, + desc: LibDescriptor, error_messages: list[str], attachments: list[str], ) -> str | None: - return _find_so_in_rel_dirs(rel_dirs, lib_searched_for, error_messages, attachments) + return _find_so_in_rel_dirs(rel_dirs, desc, self.lib_searched_for(desc), error_messages, attachments) def find_in_lib_dir( self, lib_dir: str, - _desc: LibDescriptor, - lib_searched_for: str, + desc: LibDescriptor, error_messages: list[str], attachments: list[str], ) -> str | None: - # Most libraries have both unversioned and versioned files/symlinks (exact match first) - so_name = os.path.join(lib_dir, lib_searched_for) - if os.path.isfile(so_name): - return so_name - # Some libraries only exist as versioned files (e.g., libcupti.so.13 in conda), - # so the glob fallback is needed - file_wild = lib_searched_for + "*" - # Only one match is expected, but to ensure deterministic behavior in unexpected - # situations, and to be internally consistent, we sort in reverse order with the - # intent to return the newest version first. Issue #1732 tracks the deferred - # question of raising on true ambiguity. - for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild)), reverse=True): - if os.path.isfile(so_name): - return so_name - error_messages.append(f"No such file: {file_wild}") + so_path = _find_descriptor_so_under_dir(lib_dir, desc) + if so_path is not None: + return so_path + error_messages.append(f"No such file: {self.lib_searched_for(desc)}") attachments.append(f' listdir("{lib_dir}"):') if not os.path.isdir(lib_dir): attachments.append(" DIRECTORY DOES NOT EXIST") @@ -208,8 +191,8 @@ def find_in_lib_dir( class WindowsSearchPlatform: target_arch: str - def lib_searched_for(self, libname: str) -> str: - return f"known {libname} DLL" + def lib_searched_for(self, desc: LibDescriptor) -> str: + return f"known {desc.name} DLL" def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: return cast(tuple[str, ...], desc.site_packages_windows.for_arch(self.target_arch)) @@ -241,7 +224,6 @@ def find_in_site_packages( self, rel_dirs: tuple[str, ...], desc: LibDescriptor, - lib_searched_for: str, error_messages: list[str], attachments: list[str], ) -> str | None: @@ -249,7 +231,7 @@ def find_in_site_packages( rel_dirs, desc, self.target_arch, - lib_searched_for, + self.lib_searched_for(desc), error_messages, attachments, ) @@ -258,7 +240,6 @@ def find_in_lib_dir( self, lib_dir: str, desc: LibDescriptor, - lib_searched_for: str, error_messages: list[str], attachments: list[str], ) -> str | None: @@ -266,6 +247,7 @@ def find_in_lib_dir( dll_name = _find_descriptor_dll_under_dir(lib_dir, desc, target_arch) if dll_name is not None: return dll_name + lib_searched_for = self.lib_searched_for(desc) if target_arch is None: error_messages.append(f"No such file: {lib_searched_for}") else: diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py index cad842447b1..80779f72ae2 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py @@ -59,7 +59,7 @@ def libname(self) -> str: @property def lib_searched_for(self) -> str: - return cast(str, self.platform.lib_searched_for(self.libname)) + return cast(str, self.platform.lib_searched_for(self.desc)) def raise_not_found(self) -> NoReturn: err = ", ".join(self.error_messages) @@ -102,7 +102,6 @@ def _find_using_lib_dir(ctx: SearchContext, lib_dir: str | None) -> str | None: ctx.platform.find_in_lib_dir( lib_dir, ctx.desc, - ctx.lib_searched_for, ctx.error_messages, ctx.attachments, ), @@ -195,7 +194,6 @@ def find_in_site_packages(ctx: SearchContext) -> FindResult | None: abs_path = ctx.platform.find_in_site_packages( rel_dirs, ctx.desc, - ctx.lib_searched_for, ctx.error_messages, ctx.attachments, ) diff --git a/cuda_pathfinder/tests/test_ctk_root_discovery.py b/cuda_pathfinder/tests/test_ctk_root_discovery.py index 731d38fdc0a..cac4343e3cb 100644 --- a/cuda_pathfinder/tests/test_ctk_root_discovery.py +++ b/cuda_pathfinder/tests/test_ctk_root_discovery.py @@ -73,7 +73,7 @@ def _create_nvvm_in_ctk(ctk_root): else: nvvm_dir = ctk_root / "nvvm" / "lib64" nvvm_dir.mkdir(parents=True) - nvvm_lib = nvvm_dir / "libnvvm.so" + nvvm_lib = nvvm_dir / "libnvvm.so.4" nvvm_lib.write_bytes(b"fake") return nvvm_lib @@ -91,7 +91,7 @@ def _create_cudart_in_ctk(ctk_root): else: lib_dir = ctk_root / "lib64" lib_dir.mkdir(parents=True) - lib_file = lib_dir / "libcudart.so" + lib_file = lib_dir / "libcudart.so.13" lib_file.write_bytes(b"fake") return lib_file diff --git a/cuda_pathfinder/tests/test_descriptor_catalog.py b/cuda_pathfinder/tests/test_descriptor_catalog.py index e7f68a5aceb..a4b3227018b 100644 --- a/cuda_pathfinder/tests/test_descriptor_catalog.py +++ b/cuda_pathfinder/tests/test_descriptor_catalog.py @@ -14,6 +14,7 @@ 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"} @@ -89,6 +90,12 @@ def test_linux_sonames_look_like_sonames(spec: DescriptorSpec): assert ".so" in soname, f"Unexpected Linux soname format: {soname}" +@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)) + + @pytest.mark.parametrize("spec", DESCRIPTOR_CATALOG, ids=lambda s: s.name) def test_windows_dlls_look_like_dlls(spec: DescriptorSpec): for dll in spec.windows_dlls: diff --git a/cuda_pathfinder/tests/test_lib_descriptor.py b/cuda_pathfinder/tests/test_lib_descriptor.py index 8715f9181cc..71e1f38a3c3 100644 --- a/cuda_pathfinder/tests/test_lib_descriptor.py +++ b/cuda_pathfinder/tests/test_lib_descriptor.py @@ -6,7 +6,7 @@ import pytest -from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS +from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS, linux_soname_candidates from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ( DIRECT_DEPENDENCIES, LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY, @@ -153,3 +153,18 @@ def test_descriptor_is_frozen(): desc = LIB_DESCRIPTORS["cudart"] with pytest.raises(AttributeError): desc.name = "bogus" # type: ignore[misc] + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_linux_soname_candidates_are_declared_names_newest_first(): + 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") + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_linux_soname_candidates_preserve_explicit_unversioned_name(): + desc = LIB_DESCRIPTORS["nvcudla"] + + assert linux_soname_candidates(desc) == ("libnvcudla.so",) diff --git a/cuda_pathfinder/tests/test_load_dl_linux.py b/cuda_pathfinder/tests/test_load_dl_linux.py new file mode 100644 index 00000000000..cfcf0ef29f8 --- /dev/null +++ b/cuda_pathfinder/tests/test_load_dl_linux.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys + +import pytest + +if sys.platform != "linux": + pytest.skip("Linux dynamic-loader tests", allow_module_level=True) + +from cuda.pathfinder._dynamic_libs import load_dl_linux +from cuda.pathfinder._dynamic_libs.descriptor_catalog import DescriptorSpec + + +def _descriptor() -> DescriptorSpec: + return DescriptorSpec( + name="probe", + packaged_with="other", + linux_sonames=("libprobe.so.12", "libprobe.so.13"), + ) + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_already_loaded_library_checks_only_declared_sonames_newest_first(mocker): + queried_sonames: list[tuple[str, int]] = [] + + def cdll(soname, mode): + queried_sonames.append((soname, mode)) + raise OSError + + mocker.patch.object(load_dl_linux.ctypes, "CDLL", side_effect=cdll) + + loaded = load_dl_linux.check_if_already_loaded_from_elsewhere(_descriptor()) + + assert loaded is None + assert queried_sonames == [ + ("libprobe.so.13", os.RTLD_NOLOAD), + ("libprobe.so.12", os.RTLD_NOLOAD), + ] + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_system_search_checks_only_declared_sonames_newest_first(mocker): + queried_sonames: list[str] = [] + + def load_lib(_desc, soname): + queried_sonames.append(soname) + raise OSError + + mocker.patch.object(load_dl_linux, "_load_lib", side_effect=load_lib) + + loaded = load_dl_linux.load_with_system_search(_descriptor()) + + assert loaded is None + assert queried_sonames == ["libprobe.so.13", "libprobe.so.12"] diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index dfbd66b1048..389fa6c84f4 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -95,8 +95,8 @@ def test_libname_delegates_to_descriptor(self): assert ctx.libname == "nvrtc" def test_lib_searched_for_linux(self): - ctx = SearchContext(_make_desc(name="cublas"), platform=LinuxSearchPlatform()) - assert ctx.lib_searched_for == "libcublas.so" + ctx = SearchContext(LIB_DESCRIPTORS["cublas"], platform=LinuxSearchPlatform()) + assert ctx.lib_searched_for == "libcublas.so.13 or libcublas.so.12" def test_lib_searched_for_windows(self): ctx = SearchContext(_make_desc(name="cublas"), platform=WindowsSearchPlatform(target_arch="x64")) @@ -104,7 +104,7 @@ def test_lib_searched_for_windows(self): def test_raise_not_found_includes_messages(self): ctx = _ctx() - ctx.error_messages.append("No such file: libcudart.so*") + ctx.error_messages.append("No such file: libcudart.so") ctx.attachments.append(' listdir("/some/dir"):') with pytest.raises(DynamicLibNotFoundError, match="No such file"): ctx.raise_not_found() @@ -467,15 +467,8 @@ def test_not_found_appends_error(self, mocker, tmp_path): assert result is None assert any("No such file" in m for m in ctx.error_messages) - # The next three tests cover the Linux glob fallback in - # cuda.pathfinder._dynamic_libs.search_platform._find_so_in_rel_dirs. - # The fallback triggers when the unversioned libfoo.so is absent but - # versioned libfoo.so. files exist (e.g. some conda layouts). - # Issue #1732 tracks the decision to return the newest-sorting match - # deterministically; these tests lock in that policy at the - # site-packages call site. - - def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_linux_declared_versioned_soname_does_not_need_unversioned_link(self, mocker, tmp_path): lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" lib_dir.mkdir(parents=True) versioned = lib_dir / "libcudart.so.13" @@ -486,12 +479,14 @@ def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): return_value=[str(lib_dir)], ) - result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) + desc = _make_desc(linux_sonames=("libcudart.so.12", "libcudart.so.13")) + result = find_in_site_packages(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None assert result.abs_path == str(versioned) assert result.found_via == "site-packages" - def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_linux_declared_sonames_prefer_newest(self, mocker, tmp_path): lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" lib_dir.mkdir(parents=True) older = lib_dir / "libcudart.so.12" @@ -504,25 +499,33 @@ def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path return_value=[str(lib_dir)], ) - result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform())) + desc = _make_desc(linux_sonames=("libcudart.so.12", "libcudart.so.13")) + result = find_in_site_packages(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None assert result.abs_path == str(newer) assert result.found_via == "site-packages" - def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): + @pytest.mark.parametrize( + "undeclared_filename", + ("libcudart.so", "libcudart.so.14", "libcudart.so.13.5.0", "libcudart.so.13.backup"), + ) + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_linux_rejects_undeclared_filename(self, mocker, tmp_path, undeclared_filename): lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib" lib_dir.mkdir(parents=True) - (lib_dir / "unrelated.txt").touch() + (lib_dir / undeclared_filename).touch() mocker.patch( f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", return_value=[str(lib_dir)], ) - ctx = _ctx(platform=LinuxSearchPlatform()) + desc = _make_desc(linux_sonames=("libcudart.so.12", "libcudart.so.13")) + ctx = _ctx(desc, platform=LinuxSearchPlatform()) result = find_in_site_packages(ctx) assert result is None - assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages) + assert ctx.error_messages + assert all("*" not in message for message in ctx.error_messages) # --------------------------------------------------------------------------- @@ -582,15 +585,8 @@ def test_found_windows_arm64_prefers_arch_dir(self, mocker, tmp_path): assert result.abs_path == str(arm64_dll) assert result.found_via == "conda" - # The next three tests cover the Linux glob fallback in - # cuda.pathfinder._dynamic_libs.search_platform.LinuxSearchPlatform.find_in_lib_dir, - # which is exercised by find_in_conda (and find_in_cuda_path) when the - # resolved lib dir contains only versioned libfoo.so. files. - # Issue #1732 tracks the decision to return the newest-sorting match - # deterministically; these tests lock in that policy at the conda / - # CUDA_PATH call site. - - def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_linux_declared_versioned_soname_found_in_lib_dir(self, mocker, tmp_path): lib_dir = tmp_path / "lib" lib_dir.mkdir() versioned = lib_dir / "libcudart.so.13" @@ -598,37 +594,30 @@ def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path): mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) - result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) + desc = _make_desc(linux_sonames=("libcudart.so.12", "libcudart.so.13")) + result = find_in_conda(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None assert result.abs_path == str(versioned) assert result.found_via == "conda" - def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path): - lib_dir = tmp_path / "lib" - lib_dir.mkdir() - older = lib_dir / "libcudart.so.12" - newer = lib_dir / "libcudart.so.13" - older.touch() - newer.touch() - - mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) - - result = find_in_conda(_ctx(platform=LinuxSearchPlatform())) - assert result is not None - assert result.abs_path == str(newer) - assert result.found_via == "conda" - - def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path): + @pytest.mark.parametrize( + "undeclared_filename", + ("libcudart.so", "libcudart.so.14", "libcudart.so.13.5.0", "libcudart.so.13.backup"), + ) + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_linux_rejects_undeclared_filename_in_lib_dir(self, mocker, tmp_path, undeclared_filename): lib_dir = tmp_path / "lib" lib_dir.mkdir() - (lib_dir / "unrelated.txt").touch() + (lib_dir / undeclared_filename).touch() mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)}) - ctx = _ctx(platform=LinuxSearchPlatform()) + desc = _make_desc(linux_sonames=("libcudart.so.12", "libcudart.so.13")) + ctx = _ctx(desc, platform=LinuxSearchPlatform()) result = find_in_conda(ctx) assert result is None - assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages) + assert ctx.error_messages + assert all("*" not in message for message in ctx.error_messages) # --------------------------------------------------------------------------- From 6eb600bd912f22df90b6de6ec9f817223bb99668 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Aug 2026 21:05:21 -0700 Subject: [PATCH 3/4] Declare the Linux NVVM wheel filename CUDA 12.9 NVVM wheels expose libnvvm.so without a libnvvm.so.4 alias. Declare that exact artifact name so strict descriptor-driven discovery supports the wheel without restoring an implicit fallback. --- .../_dynamic_libs/descriptor_catalog.py | 2 +- cuda_pathfinder/tests/test_lib_descriptor.py | 8 ++++++++ cuda_pathfinder/tests/test_search_steps.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py index 54c5d1b770a..6eaadc693f5 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py @@ -124,7 +124,7 @@ class DescriptorSpec: DescriptorSpec( name="nvvm", packaged_with="ctk", - linux_sonames=("libnvvm.so.4",), + linux_sonames=("libnvvm.so", "libnvvm.so.4"), windows_dlls=("nvvm64.dll", "nvvm64_40_0.dll", "nvvm70.dll"), supported_windows_arch=("x64", "arm64"), site_packages_linux=("nvidia/cu13/lib", "nvidia/cuda_nvcc/nvvm/lib64"), diff --git a/cuda_pathfinder/tests/test_lib_descriptor.py b/cuda_pathfinder/tests/test_lib_descriptor.py index 71e1f38a3c3..cdfac333c9d 100644 --- a/cuda_pathfinder/tests/test_lib_descriptor.py +++ b/cuda_pathfinder/tests/test_lib_descriptor.py @@ -168,3 +168,11 @@ def test_linux_soname_candidates_preserve_explicit_unversioned_name(): desc = LIB_DESCRIPTORS["nvcudla"] assert linux_soname_candidates(desc) == ("libnvcudla.so",) + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_linux_soname_candidates_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") diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 389fa6c84f4..85f60861dab 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -289,6 +289,24 @@ def test_found_linux(self, mocker, tmp_path): assert result.abs_path == str(so_file) assert result.found_via == "site-packages" + @pytest.mark.agent_authored(model="gpt-5.6-sol") + def test_nvvm_linux_wheel_accepts_declared_unversioned_filename(self, mocker, tmp_path): + lib_dir = tmp_path / "nvidia" / "cuda_nvcc" / "nvvm" / "lib64" + lib_dir.mkdir(parents=True) + so_file = lib_dir / "libnvvm.so" + so_file.touch() + + mocker.patch( + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + return_value=[str(lib_dir)], + ) + + result = find_in_site_packages(_ctx(LIB_DESCRIPTORS["nvvm"], platform=LinuxSearchPlatform())) + + assert result is not None + assert result.abs_path == str(so_file) + assert result.found_via == "site-packages" + def test_found_windows(self, mocker, tmp_path): bin_dir = tmp_path / "nvidia" / "cuda_runtime" / "bin" bin_dir.mkdir(parents=True) From aae8753121ef7c65e7566306e17bc373b21fcb40 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Aug 2026 22:20:06 -0700 Subject: [PATCH 4/4] docs(pathfinder): prepare 1.7.1 release notes --- cuda_pathfinder/docs/nv-versions.json | 4 ++++ .../docs/source/release/1.7.1-notes.rst | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 cuda_pathfinder/docs/source/release/1.7.1-notes.rst diff --git a/cuda_pathfinder/docs/nv-versions.json b/cuda_pathfinder/docs/nv-versions.json index 48b45fdf873..6c629101974 100644 --- a/cuda_pathfinder/docs/nv-versions.json +++ b/cuda_pathfinder/docs/nv-versions.json @@ -3,6 +3,10 @@ "version": "latest", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/" }, + { + "version": "1.7.1", + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.7.1/" + }, { "version": "1.7.0", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.7.0/" diff --git a/cuda_pathfinder/docs/source/release/1.7.1-notes.rst b/cuda_pathfinder/docs/source/release/1.7.1-notes.rst new file mode 100644 index 00000000000..edfd4862544 --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.7.1-notes.rst @@ -0,0 +1,24 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.7.1 Release notes +======================================= + +Bugfixes +-------- + +* Restrict Windows CUPTI filesystem discovery to descriptor-declared DLL + names. Removing broad wildcard matches keeps candidate selection consistent + with already-loaded detection and native loading, and prevents undeclared + DLL names from being selected. + (`PR #2689 `_) + +* Make Linux filesystem discovery, already-loaded detection, and native loading + use one descriptor-declared, newest-first library filename list. This prevents + broad matches from selecting undeclared ABI or patch-version filenames; + explicit directory layouts must expose a declared name. Explicitly declare + the unversioned NVVM 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 `_)