Skip to content

Commit 6d6d245

Browse files
committed
Merge branch 'main' into stop_using_cuda-bindings_from_pypi
2 parents 71502ad + 21286b0 commit 6d6d245

22 files changed

Lines changed: 815 additions & 666 deletions

cuda_pathfinder/cuda/pathfinder/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@
2020
)
2121
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL as LoadedDL
2222
from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import load_nvidia_dynamic_lib as load_nvidia_dynamic_lib
23-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
24-
SUPPORTED_LIBNAMES as SUPPORTED_NVIDIA_LIBNAMES,
25-
)
23+
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LIBNAMES as _SUPPORTED_NVIDIA_LIBNAMES
2624
from cuda.pathfinder._headers.find_nvidia_headers import LocatedHeaderDir as LocatedHeaderDir
2725
from cuda.pathfinder._headers.find_nvidia_headers import find_nvidia_header_directory as find_nvidia_header_directory
2826
from cuda.pathfinder._headers.find_nvidia_headers import (
@@ -60,6 +58,7 @@
6058
locate_static_lib as locate_static_lib,
6159
)
6260
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home as get_cuda_path_or_home
61+
from cuda.pathfinder._utils.windows_arch import UnsupportedArchError as UnsupportedArchError
6362

6463
from cuda.pathfinder._version import __version__ # isort: skip
6564

@@ -76,6 +75,11 @@
7675
#: Example utilities: ``"nvdisasm"``, ``"cuobjdump"``, ``"nvcc"``.
7776
SUPPORTED_BINARY_UTILITIES = _SUPPORTED_BINARIES
7877

78+
#: Tuple of CUDA Toolkit dynamic library names supported by
79+
#: :func:`load_nvidia_dynamic_lib` for the current operating system and
80+
#: interpreter architecture.
81+
SUPPORTED_NVIDIA_LIBNAMES = _SUPPORTED_NVIDIA_LIBNAMES
82+
7983
#: Tuple of supported bitcode library names that can be resolved
8084
#: via ``locate_bitcode_lib()`` and ``find_bitcode_lib()``.
8185
#: Example value: ``"device"``.

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py

Lines changed: 140 additions & 39 deletions
Large diffs are not rendered by default.

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
build_dynamic_lib_subprocess_command,
3535
parse_dynamic_lib_subprocess_payload,
3636
)
37+
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ALL_AVAILABLE_LIBNAMES
3738
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
3839

3940
if TYPE_CHECKING:
@@ -42,9 +43,6 @@
4243
# All libnames recognized by load_nvidia_dynamic_lib, across all categories
4344
# (CTK, third-party, driver).
4445
_ALL_KNOWN_LIBNAMES: frozenset[str] = frozenset(LIB_DESCRIPTORS)
45-
_ALL_SUPPORTED_LIBNAMES: frozenset[str] = frozenset(
46-
name for name, desc in LIB_DESCRIPTORS.items() if (desc.windows_dlls if IS_WINDOWS else desc.linux_sonames)
47-
)
4846
_PLATFORM_NAME = "Windows" if IS_WINDOWS else "Linux"
4947
_CANARY_PROBE_TIMEOUT_SECONDS = 10.0
5048

@@ -308,9 +306,9 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL:
308306
)
309307
if libname not in _ALL_KNOWN_LIBNAMES:
310308
raise DynamicLibUnknownError(f"Unknown library name: {libname!r}. Known names: {sorted(_ALL_KNOWN_LIBNAMES)}")
311-
if libname not in _ALL_SUPPORTED_LIBNAMES:
309+
if libname not in ALL_AVAILABLE_LIBNAMES:
312310
raise DynamicLibNotAvailableError(
313311
f"Library name {libname!r} is known but not available on {_PLATFORM_NAME}. "
314-
f"Supported names on {_PLATFORM_NAME}: {sorted(_ALL_SUPPORTED_LIBNAMES)}"
312+
f"Supported names on {_PLATFORM_NAME}: {sorted(ALL_AVAILABLE_LIBNAMES)}"
315313
)
316314
return _load_lib_no_cache(libname)

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
import os
1515
from collections.abc import Sequence
1616
from dataclasses import dataclass
17+
from pathlib import PurePath
1718
from typing import Protocol, cast
1819

1920
from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
2021
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import is_suppressed_dll_file
2122
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
2223
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
24+
from cuda.pathfinder._utils.windows_arch import windows_pe_matches_arch, windows_python_arch
2325

2426

2527
def _no_such_file_in_sub_dirs(
@@ -41,7 +43,7 @@ def _find_so_in_rel_dirs(
4143
sub_dirs_searched: list[tuple[str, ...]] = []
4244
file_wild = so_basename + "*"
4345
for rel_dir in rel_dirs:
44-
sub_dir = tuple(rel_dir.split(os.path.sep))
46+
sub_dir = PurePath(rel_dir).parts
4547
for abs_dir in find_sub_dirs_all_sitepackages(sub_dir):
4648
# Exact unversioned match first; fall back to versioned names because some
4749
# distros only ship lib<name>.so.<major> (e.g. conda libcupti). Only one match
@@ -61,12 +63,15 @@ def _find_so_in_rel_dirs(
6163
return None
6264

6365

64-
def _find_dll_under_dir(dirpath: str, file_wild: str) -> str | None:
66+
def _find_dll_under_dir(dirpath: str, file_wild: str, target_arch: str | None = None) -> str | None:
6567
for path in sorted(glob.glob(os.path.join(dirpath, file_wild))):
6668
if not os.path.isfile(path):
6769
continue
68-
if not is_suppressed_dll_file(os.path.basename(path)):
69-
return path
70+
if is_suppressed_dll_file(os.path.basename(path)):
71+
continue
72+
if target_arch is not None and not windows_pe_matches_arch(path, target_arch):
73+
continue
74+
return path
7075
return None
7176

7277

@@ -78,7 +83,7 @@ def _find_dll_in_rel_dirs(
7883
) -> str | None:
7984
sub_dirs_searched: list[tuple[str, ...]] = []
8085
for rel_dir in rel_dirs:
81-
sub_dir = tuple(rel_dir.split(os.path.sep))
86+
sub_dir = PurePath(rel_dir).parts
8287
for abs_dir in find_sub_dirs_all_sitepackages(sub_dir):
8388
dll_name = _find_dll_under_dir(abs_dir, lib_searched_for)
8489
if dll_name is not None:
@@ -109,7 +114,7 @@ def find_in_site_packages(
109114
def find_in_lib_dir(
110115
self,
111116
lib_dir: str,
112-
libname: str,
117+
desc: LibDescriptor,
113118
lib_searched_for: str,
114119
error_messages: list[str],
115120
attachments: list[str],
@@ -142,7 +147,7 @@ def find_in_site_packages(
142147
def find_in_lib_dir(
143148
self,
144149
lib_dir: str,
145-
_libname: str,
150+
_desc: LibDescriptor,
146151
lib_searched_for: str,
147152
error_messages: list[str],
148153
attachments: list[str],
@@ -173,17 +178,19 @@ def find_in_lib_dir(
173178

174179
@dataclass(frozen=True, slots=True)
175180
class WindowsSearchPlatform:
181+
target_arch: str
182+
176183
def lib_searched_for(self, libname: str) -> str:
177184
return f"{libname}*.dll"
178185

179186
def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
180-
return cast(tuple[str, ...], desc.site_packages_windows)
187+
return cast(tuple[str, ...], desc.site_packages_windows.for_arch(self.target_arch))
181188

182189
def conda_anchor_point(self, conda_prefix: str) -> str:
183190
return os.path.join(conda_prefix, "Library")
184191

185192
def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
186-
return cast(tuple[str, ...], desc.anchor_rel_dirs_windows)
193+
return cast(tuple[str, ...], desc.anchor_rel_dirs_windows.for_arch(self.target_arch))
187194

188195
def find_in_site_packages(
189196
self,
@@ -197,16 +204,20 @@ def find_in_site_packages(
197204
def find_in_lib_dir(
198205
self,
199206
lib_dir: str,
200-
libname: str,
207+
desc: LibDescriptor,
201208
_lib_searched_for: str,
202209
error_messages: list[str],
203210
attachments: list[str],
204211
) -> str | None:
205-
file_wild = libname + "*.dll"
206-
dll_name = _find_dll_under_dir(lib_dir, file_wild)
212+
file_wild = desc.name + "*.dll"
213+
target_arch = self.target_arch if desc.requires_windows_binary_arch_check else None
214+
dll_name = _find_dll_under_dir(lib_dir, file_wild, target_arch)
207215
if dll_name is not None:
208216
return dll_name
209-
error_messages.append(f"No such file: {file_wild}")
217+
if target_arch is None:
218+
error_messages.append(f"No such file: {file_wild}")
219+
else:
220+
error_messages.append(f"No {target_arch}-compatible PE file: {file_wild}")
210221
attachments.append(f' listdir("{lib_dir}"):')
211222
if not os.path.isdir(lib_dir):
212223
attachments.append(" DIRECTORY DOES NOT EXIST")
@@ -216,4 +227,10 @@ def find_in_lib_dir(
216227
return None
217228

218229

219-
PLATFORM: SearchPlatform = WindowsSearchPlatform() if IS_WINDOWS else LinuxSearchPlatform()
230+
def _platform_for_current_system() -> SearchPlatform:
231+
if IS_WINDOWS:
232+
return WindowsSearchPlatform(target_arch=windows_python_arch())
233+
return LinuxSearchPlatform()
234+
235+
236+
PLATFORM = _platform_for_current_system()

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def _find_using_lib_dir(ctx: SearchContext, lib_dir: str | None) -> str | None:
8888
str | None,
8989
ctx.platform.find_in_lib_dir(
9090
lib_dir,
91-
ctx.libname,
91+
ctx.desc,
9292
ctx.lib_searched_for,
9393
ctx.error_messages,
9494
ctx.attachments,
@@ -121,13 +121,14 @@ def _derive_ctk_root_windows(resolved_lib_path: str) -> str | None:
121121
122122
Supports:
123123
- ``$CTK_ROOT/bin/x64/foo.dll`` (CTK 13 style)
124+
- ``$CTK_ROOT/bin/arm64/foo.dll`` (Windows on Arm CTK 13 style)
124125
- ``$CTK_ROOT/bin/foo.dll`` (CTK 12 style)
125126
"""
126127
import ntpath
127128

128129
lib_dir = ntpath.dirname(resolved_lib_path)
129130
basename = ntpath.basename(lib_dir).lower()
130-
if basename == "x64":
131+
if basename in ("x64", "arm64"):
131132
parent = ntpath.dirname(lib_dir)
132133
if ntpath.basename(parent).lower() == "bin":
133134
return ntpath.dirname(parent)

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/supported_nvidia_libs.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
The canonical data entry point is :mod:`descriptor_catalog`. This module keeps
77
historical constant names for backward compatibility by deriving them from the
88
catalog.
9+
10+
The unsuffixed ``SUPPORTED_LIBNAMES_WINDOWS`` and
11+
``SITE_PACKAGES_LIBDIRS_WINDOWS*`` constants retain their historical x64
12+
meaning for compatibility, but are not recommended for new code. Use the
13+
explicit ``*_X64`` or ``*_ARM64`` projection instead. Never combine the two
14+
architecture projections.
915
"""
1016

1117
from __future__ import annotations
1218

1319
from cuda.pathfinder._dynamic_libs.descriptor_catalog import DESCRIPTOR_CATALOG
14-
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
20+
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS, IS_WINDOWS_ARM64, IS_WINDOWS_X64
1521

1622
_CTK_DESCRIPTORS = tuple(desc for desc in DESCRIPTOR_CATALOG if desc.packaged_with == "ctk")
1723
_OTHER_DESCRIPTORS = tuple(desc for desc in DESCRIPTOR_CATALOG if desc.packaged_with == "other")
@@ -26,10 +32,30 @@
2632
desc.name for desc in _CTK_DESCRIPTORS if desc.windows_dlls and not desc.linux_sonames
2733
)
2834

35+
if not IS_WINDOWS:
36+
ALL_AVAILABLE_LIBNAMES = frozenset(desc.name for desc in DESCRIPTOR_CATALOG if desc.linux_sonames)
37+
else:
38+
assert IS_WINDOWS_X64 != IS_WINDOWS_ARM64
39+
_current_windows_arch = "x64" if IS_WINDOWS_X64 else "arm64"
40+
ALL_AVAILABLE_LIBNAMES = frozenset(
41+
desc.name for desc in DESCRIPTOR_CATALOG if _current_windows_arch in desc.supported_windows_arch
42+
)
43+
2944
SUPPORTED_LIBNAMES_LINUX = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_LINUX_ONLY
30-
SUPPORTED_LIBNAMES_WINDOWS = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_WINDOWS_ONLY
45+
SUPPORTED_LIBNAMES_WINDOWS_X64 = tuple(desc.name for desc in _CTK_DESCRIPTORS if "x64" in desc.supported_windows_arch)
46+
SUPPORTED_LIBNAMES_WINDOWS_ARM64 = tuple(
47+
desc.name for desc in _CTK_DESCRIPTORS if "arm64" in desc.supported_windows_arch
48+
)
49+
# Backward-compatible alias preserves the historical x64 meaning.
50+
SUPPORTED_LIBNAMES_WINDOWS = SUPPORTED_LIBNAMES_WINDOWS_X64
3151
SUPPORTED_LIBNAMES_ALL = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_LINUX_ONLY + SUPPORTED_LIBNAMES_WINDOWS_ONLY
32-
SUPPORTED_LIBNAMES = SUPPORTED_LIBNAMES_WINDOWS if IS_WINDOWS else SUPPORTED_LIBNAMES_LINUX
52+
if not IS_WINDOWS:
53+
SUPPORTED_LIBNAMES = SUPPORTED_LIBNAMES_LINUX
54+
elif IS_WINDOWS_X64:
55+
SUPPORTED_LIBNAMES = SUPPORTED_LIBNAMES_WINDOWS_X64
56+
else:
57+
assert IS_WINDOWS_ARM64
58+
SUPPORTED_LIBNAMES = SUPPORTED_LIBNAMES_WINDOWS_ARM64
3359

3460
DIRECT_DEPENDENCIES_CTK = {desc.name: desc.dependencies for desc in _CTK_DESCRIPTORS if desc.dependencies}
3561
DIRECT_DEPENDENCIES = {desc.name: desc.dependencies for desc in DESCRIPTOR_CATALOG if desc.dependencies}
@@ -51,7 +77,6 @@
5177
desc.name for desc in DESCRIPTOR_CATALOG if desc.requires_rtld_deepbind and desc.linux_sonames
5278
)
5379

54-
# Based on output of toolshed/make_site_packages_libdirs_linux.py
5580
SITE_PACKAGES_LIBDIRS_LINUX_CTK = {
5681
desc.name: desc.site_packages_linux for desc in _CTK_DESCRIPTORS if desc.site_packages_linux
5782
}
@@ -60,13 +85,29 @@
6085
}
6186
SITE_PACKAGES_LIBDIRS_LINUX = SITE_PACKAGES_LIBDIRS_LINUX_CTK | SITE_PACKAGES_LIBDIRS_LINUX_OTHER
6287

63-
SITE_PACKAGES_LIBDIRS_WINDOWS_CTK = {
64-
desc.name: desc.site_packages_windows for desc in _CTK_DESCRIPTORS if desc.site_packages_windows
88+
# Architecture-specific Windows projections. Keep these separate: combining
89+
# them would make the table unsafe to consume for either process ABI.
90+
SITE_PACKAGES_LIBDIRS_WINDOWS_CTK_X64 = {
91+
desc.name: desc.site_packages_windows.x64 for desc in _CTK_DESCRIPTORS if desc.site_packages_windows.x64
92+
}
93+
SITE_PACKAGES_LIBDIRS_WINDOWS_CTK_ARM64 = {
94+
desc.name: desc.site_packages_windows.arm64 for desc in _CTK_DESCRIPTORS if desc.site_packages_windows.arm64
6595
}
66-
SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER = {
67-
desc.name: desc.site_packages_windows for desc in _NON_CTK_DESCRIPTORS if desc.site_packages_windows
96+
SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER_X64 = {
97+
desc.name: desc.site_packages_windows.x64 for desc in _NON_CTK_DESCRIPTORS if desc.site_packages_windows.x64
6898
}
69-
SITE_PACKAGES_LIBDIRS_WINDOWS = SITE_PACKAGES_LIBDIRS_WINDOWS_CTK | SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER
99+
SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER_ARM64 = {
100+
desc.name: desc.site_packages_windows.arm64 for desc in _NON_CTK_DESCRIPTORS if desc.site_packages_windows.arm64
101+
}
102+
SITE_PACKAGES_LIBDIRS_WINDOWS_X64 = SITE_PACKAGES_LIBDIRS_WINDOWS_CTK_X64 | SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER_X64
103+
SITE_PACKAGES_LIBDIRS_WINDOWS_ARM64 = (
104+
SITE_PACKAGES_LIBDIRS_WINDOWS_CTK_ARM64 | SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER_ARM64
105+
)
106+
107+
# Backward-compatible aliases preserve the historical x64 meaning.
108+
SITE_PACKAGES_LIBDIRS_WINDOWS_CTK = SITE_PACKAGES_LIBDIRS_WINDOWS_CTK_X64
109+
SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER = SITE_PACKAGES_LIBDIRS_WINDOWS_OTHER_X64
110+
SITE_PACKAGES_LIBDIRS_WINDOWS = SITE_PACKAGES_LIBDIRS_WINDOWS_X64
70111

71112

72113
def is_suppressed_dll_file(path_basename: str) -> bool:

cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
import sys
55

66
IS_WINDOWS = sys.platform == "win32"
7+
_WINDOWS_PYTHON_ARCH: str | None
8+
9+
if IS_WINDOWS:
10+
from cuda.pathfinder._utils.windows_arch import windows_python_arch
11+
12+
_WINDOWS_PYTHON_ARCH = windows_python_arch()
13+
else:
14+
_WINDOWS_PYTHON_ARCH = None
15+
16+
# These describe the Python process ABI, not the Windows host architecture.
17+
IS_WINDOWS_X64 = _WINDOWS_PYTHON_ARCH == "x64"
18+
IS_WINDOWS_ARM64 = _WINDOWS_PYTHON_ARCH == "arm64"
719

820

921
def quote_for_shell(s: str) -> str:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import sysconfig
7+
8+
WINDOWS_PE_MACHINE_BY_ARCH = {
9+
"x64": 0x8664,
10+
"arm64": 0xAA64,
11+
}
12+
13+
14+
class UnsupportedArchError(RuntimeError):
15+
"""Raised when Python reports an unsupported Windows architecture."""
16+
17+
def __init__(self, platform_tag: str) -> None:
18+
self.platform_tag = platform_tag
19+
super().__init__(
20+
f"Unsupported Windows Python platform tag: {platform_tag!r}; expected 'win-amd64' or 'win-arm64'"
21+
)
22+
23+
24+
def windows_python_arch() -> str:
25+
"""Return the current Windows Python interpreter architecture."""
26+
raw_platform_tag = sysconfig.get_platform()
27+
platform_tag = raw_platform_tag.lower().replace("_", "-")
28+
29+
if platform_tag == "win-arm64":
30+
return "arm64"
31+
32+
if platform_tag == "win-amd64":
33+
return "x64"
34+
35+
raise UnsupportedArchError(raw_platform_tag)
36+
37+
38+
def windows_pe_matches_arch(path: str, target_arch: str) -> bool:
39+
"""Return whether a Windows Portable Executable (PE) targets the requested architecture.
40+
41+
PE is the file format used for Windows executables and DLLs. This reads the
42+
PE/COFF header's machine field to distinguish x64 images from Arm64 images.
43+
"""
44+
expected_machine = WINDOWS_PE_MACHINE_BY_ARCH.get(target_arch)
45+
if expected_machine is None:
46+
raise ValueError(f"Unsupported Windows target architecture: {target_arch!r}")
47+
48+
try:
49+
with open(path, "rb") as stream:
50+
if stream.read(2) != b"MZ":
51+
return False
52+
stream.seek(0x3C)
53+
pe_offset_bytes = stream.read(4)
54+
if len(pe_offset_bytes) != 4:
55+
return False
56+
stream.seek(int.from_bytes(pe_offset_bytes, "little"))
57+
if stream.read(4) != b"PE\0\0":
58+
return False
59+
machine_bytes = stream.read(2)
60+
if len(machine_bytes) != 2:
61+
return False
62+
except OSError:
63+
return False
64+
65+
return int.from_bytes(machine_bytes, "little") == expected_machine

0 commit comments

Comments
 (0)