Skip to content
Draft
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 @@ -59,9 +59,9 @@ 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, ...] = ()
windows_dll_fallback_globs: tuple[str, ...] = ()
supported_windows_arch: tuple[WindowsArch, ...] = ()
site_packages_linux: tuple[str, ...] = ()
site_packages_windows: WindowsSearchDirs = WindowsSearchDirs()
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -377,7 +377,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"),
Expand Down Expand Up @@ -414,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
12 changes: 3 additions & 9 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
98 changes: 35 additions & 63 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.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-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Platform abstraction for filesystem search steps.
Expand All @@ -10,57 +10,54 @@

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.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


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<name>.so.<major> (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


Expand All @@ -76,19 +73,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


Expand All @@ -115,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, ...]: ...

Expand All @@ -133,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: ...
Expand All @@ -142,16 +130,15 @@ def find_in_lib_dir(
self,
lib_dir: str,
desc: LibDescriptor,
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None: ...


@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)
Expand All @@ -174,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")
Expand All @@ -217,8 +191,8 @@ def find_in_lib_dir(
class WindowsSearchPlatform:
target_arch: str

def lib_searched_for(self, libname: str) -> str:
return f"{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))
Expand Down Expand Up @@ -250,15 +224,14 @@ 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:
return _find_dll_in_rel_dirs(
rel_dirs,
desc,
self.target_arch,
lib_searched_for,
self.lib_searched_for(desc),
error_messages,
attachments,
)
Expand All @@ -267,19 +240,18 @@ def find_in_lib_dir(
self,
lib_dir: str,
desc: LibDescriptor,
_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
lib_searched_for = self.lib_searched_for(desc)
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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -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,
)
Expand Down
4 changes: 4 additions & 0 deletions cuda_pathfinder/docs/nv-versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
24 changes: 24 additions & 0 deletions cuda_pathfinder/docs/source/release/1.7.1-notes.rst
Original file line number Diff line number Diff line change
@@ -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 <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. 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 <https://github.com/NVIDIA/cuda-python/pull/2689>`_)
4 changes: 2 additions & 2 deletions cuda_pathfinder/tests/test_ctk_root_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading
Loading