From cb54cf609028b0d22153afb2479cce28df5a6443 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 10 Apr 2026 16:36:23 -0700 Subject: [PATCH 01/18] Add initial pathfinder compatibility checks Introduce a minimal WithCompatibilityChecks API with CTK version guard rails, including wheel metadata support and wrapper-first tests. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/__init__.py | 9 + .../cuda/pathfinder/_compatibility.py | 575 ++++++++++++++++++ cuda_pathfinder/docs/source/api.rst | 4 + .../tests/test_with_compatibility_checks.py | 309 ++++++++++ 4 files changed, 897 insertions(+) create mode 100644 cuda_pathfinder/cuda/pathfinder/_compatibility.py create mode 100644 cuda_pathfinder/tests/test_with_compatibility_checks.py diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index dc818dfd08f..e4451347535 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -11,6 +11,15 @@ find_nvidia_binary_utility as find_nvidia_binary_utility, ) from cuda.pathfinder._binaries.supported_nvidia_binaries import SUPPORTED_BINARIES as _SUPPORTED_BINARIES +from cuda.pathfinder._compatibility import ( + CompatibilityCheckError as CompatibilityCheckError, +) +from cuda.pathfinder._compatibility import ( + CompatibilityInsufficientMetadataError as CompatibilityInsufficientMetadataError, +) +from cuda.pathfinder._compatibility import ( + WithCompatibilityChecks as WithCompatibilityChecks, +) from cuda.pathfinder._dynamic_libs.load_dl_common import ( DynamicLibNotAvailableError as DynamicLibNotAvailableError, ) diff --git a/cuda_pathfinder/cuda/pathfinder/_compatibility.py b/cuda_pathfinder/cuda/pathfinder/_compatibility.py new file mode 100644 index 00000000000..ec185bec444 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_compatibility.py @@ -0,0 +1,575 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import ctypes +import functools +import importlib.metadata +import json +import os +import re +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import TypeAlias, cast + +from cuda.pathfinder._binaries.find_nvidia_binary_utility import ( + find_nvidia_binary_utility as _find_nvidia_binary_utility, +) +from cuda.pathfinder._binaries.supported_nvidia_binaries import SUPPORTED_BINARIES_ALL +from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL +from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( + load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib, +) +from cuda.pathfinder._headers.find_nvidia_headers import ( + LocatedHeaderDir, +) +from cuda.pathfinder._headers.find_nvidia_headers import ( + locate_nvidia_header_directory as _locate_nvidia_header_directory, +) +from cuda.pathfinder._headers.header_descriptor import HEADER_DESCRIPTORS +from cuda.pathfinder._static_libs.find_bitcode_lib import ( + LocatedBitcodeLib, +) +from cuda.pathfinder._static_libs.find_bitcode_lib import ( + locate_bitcode_lib as _locate_bitcode_lib, +) +from cuda.pathfinder._static_libs.find_static_lib import ( + LocatedStaticLib, +) +from cuda.pathfinder._static_libs.find_static_lib import ( + locate_static_lib as _locate_static_lib, +) +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + +ItemKind: TypeAlias = str +PackagedWith: TypeAlias = str +ConstraintOperator: TypeAlias = str +ConstraintArg: TypeAlias = int | str | tuple[str, int] | None +DriverVersionArg: TypeAlias = int | None + +_CTK_VERSION_RE = re.compile(r"^(?P\d+)\.(?P\d+)") +_REQUIRES_DIST_RE = re.compile( + r"^\s*(?P[A-Za-z0-9_.-]+)\s*==\s*(?P[0-9][A-Za-z0-9.+-]*?)(?:\.\*)?(?:\s*;|$)" +) + +_STATIC_LIBS_PACKAGED_WITH: dict[str, PackagedWith] = { + "cudadevrt": "ctk", +} +_BITCODE_LIBS_PACKAGED_WITH: dict[str, PackagedWith] = { + "device": "ctk", + "nvshmem_device": "other", +} +_BINARY_PACKAGED_WITH: dict[str, PackagedWith] = dict.fromkeys(SUPPORTED_BINARIES_ALL, "ctk") + + +class CompatibilityCheckError(RuntimeError): + """Raised when compatibility checks reject a resolved item.""" + + +class CompatibilityInsufficientMetadataError(CompatibilityCheckError): + """Raised when v1 compatibility checks cannot reach a definitive answer.""" + + +@dataclass(frozen=True, slots=True) +class CtkMetadata: + ctk_version: CtkVersion + ctk_root: str | None + source: str + + +@dataclass(frozen=True, slots=True) +class CtkVersion: + major: int + minor: int + + def __str__(self) -> str: + return f"{self.major}.{self.minor}" + + +@dataclass(frozen=True, slots=True) +class ComparisonConstraint: + operator: ConstraintOperator + value: int + + def matches(self, candidate: int) -> bool: + if self.operator == "==": + return candidate == self.value + if self.operator == "<": + return candidate < self.value + if self.operator == "<=": + return candidate <= self.value + if self.operator == ">": + return candidate > self.value + if self.operator == ">=": + return candidate >= self.value + raise AssertionError(f"Unsupported operator: {self.operator!r}") + + def __str__(self) -> str: + return f"{self.operator}{self.value}" + + +@dataclass(frozen=True, slots=True) +class ResolvedItem: + name: str + kind: ItemKind + packaged_with: PackagedWith + abs_path: str + found_via: str | None + ctk_root: str | None + ctk_version: CtkVersion | None + ctk_version_source: str | None + + def describe(self) -> str: + found_via = "" if self.found_via is None else f" via {self.found_via}" + return f"{self.kind} {self.name!r}{found_via} at {self.abs_path!r}" + + +@dataclass(frozen=True, slots=True) +class CompatibilityResult: + status: str + message: str + + def require_compatible(self) -> None: + if self.status == "compatible": + return + if self.status == "insufficient_metadata": + raise CompatibilityInsufficientMetadataError(self.message) + raise CompatibilityCheckError(self.message) + + +def _coerce_constraint(name: str, raw_value: ConstraintArg) -> ComparisonConstraint | None: + if raw_value is None: + return None + if isinstance(raw_value, int): + return ComparisonConstraint("==", raw_value) + if isinstance(raw_value, tuple): + if len(raw_value) != 2: + raise ValueError(f"{name} tuple constraints must have exactly two elements.") + operator, value = raw_value + if operator not in ("==", "<", "<=", ">", ">="): + raise ValueError(f"{name} has unsupported operator {operator!r}.") + if not isinstance(value, int): + raise ValueError(f"{name} constraint value must be an integer.") + return ComparisonConstraint(operator, value) + if isinstance(raw_value, str): + match = re.fullmatch(r"\s*(==|<|<=|>|>=)?\s*(\d+)\s*", raw_value) + if match is None: + raise ValueError(f"{name} must be an int, a (operator, value) tuple, or a string like '>=12'.") + operator = match.group(1) or "==" + value = int(match.group(2)) + return ComparisonConstraint(operator, value) + raise ValueError(f"{name} must be an int, a (operator, value) tuple, or a string like '>=12'.") + + +def _driver_major(driver_version: int) -> int: + return driver_version // 1000 + + +def _parse_ctk_version(cuda_version: str) -> CtkVersion | None: + match = _CTK_VERSION_RE.match(cuda_version) + if match is None: + return None + return CtkVersion(major=int(match.group("major")), minor=int(match.group("minor"))) + + +def _normalize_distribution_name(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def _distribution_name(dist: importlib.metadata.Distribution) -> str | None: + # Work around mypy's typing of Distribution.metadata as PackageMetadata: + # the runtime object behaves like a string mapping, but mypy does not + # expose Mapping.get() on PackageMetadata. + metadata = cast(Mapping[str, str], dist.metadata) + return metadata.get("Name") + + +@functools.cache +def _owned_distribution_candidates(abs_path: str) -> tuple[tuple[str, str], ...]: + normalized_abs_path = os.path.normpath(os.path.abspath(abs_path)) + matches: set[tuple[str, str]] = set() + for dist in importlib.metadata.distributions(): + dist_name = _distribution_name(dist) + if not dist_name: + continue + for file in dist.files or (): + candidate_abs_path = os.path.normpath(os.path.abspath(str(dist.locate_file(file)))) + if candidate_abs_path == normalized_abs_path: + matches.add((dist_name, dist.version)) + return tuple(sorted(matches)) + + +@functools.cache +def _cuda_toolkit_requirement_maps() -> tuple[tuple[str, CtkVersion, dict[str, tuple[str, ...]]], ...]: + results: list[tuple[str, CtkVersion, dict[str, tuple[str, ...]]]] = [] + for dist in importlib.metadata.distributions(): + dist_name = _distribution_name(dist) + if _normalize_distribution_name(dist_name or "") != "cuda-toolkit": + continue + ctk_version = _parse_ctk_version(dist.version) + if ctk_version is None: + continue + requirement_map: dict[str, set[str]] = {} + for requirement in dist.requires or (): + match = _REQUIRES_DIST_RE.match(requirement) + if match is None: + continue + req_name = _normalize_distribution_name(match.group("name")) + requirement_map.setdefault(req_name, set()).add(match.group("version")) + results.append( + ( + dist.version, + ctk_version, + {name: tuple(sorted(prefixes)) for name, prefixes in requirement_map.items()}, + ) + ) + return tuple(results) + + +def _wheel_metadata_for_abs_path(abs_path: str) -> CtkMetadata | None: + matched_versions: dict[CtkVersion, str] = {} + for owner_name, owner_version in _owned_distribution_candidates(abs_path): + normalized_owner_name = _normalize_distribution_name(owner_name) + for toolkit_dist_version, ctk_version, requirement_map in _cuda_toolkit_requirement_maps(): + requirement_prefixes = requirement_map.get(normalized_owner_name, ()) + if not any( + owner_version == prefix or owner_version.startswith(prefix + ".") for prefix in requirement_prefixes + ): + continue + matched_versions[ctk_version] = ( + f"wheel metadata via {owner_name}=={owner_version} pinned by cuda-toolkit=={toolkit_dist_version}" + ) + if len(matched_versions) != 1: + return None + [(ctk_version, source)] = matched_versions.items() + return CtkMetadata(ctk_version=ctk_version, ctk_root=None, source=source) + + +@functools.cache +def _read_ctk_version(ctk_root: str) -> CtkVersion | None: + version_json_path = os.path.join(ctk_root, "version.json") + if not os.path.isfile(version_json_path): + return None + with open(version_json_path, encoding="utf-8") as fobj: + payload = json.load(fobj) + if not isinstance(payload, dict): + return None + cuda_entry = payload.get("cuda") + if not isinstance(cuda_entry, dict): + return None + cuda_version = cuda_entry.get("version") + if not isinstance(cuda_version, str): + return None + return _parse_ctk_version(cuda_version) + + +def _find_enclosing_ctk_root(abs_path: str) -> str | None: + current = Path(abs_path) + if current.is_file(): + current = current.parent + for candidate in (current, *current.parents): + ctk_root = str(candidate) + if _read_ctk_version(ctk_root) is not None: + return ctk_root + return None + + +def _ctk_metadata_for_abs_path(abs_path: str) -> CtkMetadata | None: + ctk_root = _find_enclosing_ctk_root(abs_path) + if ctk_root is not None: + ctk_version = _read_ctk_version(ctk_root) + if ctk_version is not None: + version_json_path = os.path.join(ctk_root, "version.json") + return CtkMetadata( + ctk_version=ctk_version, + ctk_root=ctk_root, + source=f"version.json at {version_json_path}", + ) + return _wheel_metadata_for_abs_path(abs_path) + + +def _resolve_item( + *, + name: str, + kind: ItemKind, + packaged_with: PackagedWith, + abs_path: str, + found_via: str | None, +) -> ResolvedItem: + ctk_metadata = _ctk_metadata_for_abs_path(abs_path) + return ResolvedItem( + name=name, + kind=kind, + packaged_with=packaged_with, + abs_path=abs_path, + found_via=found_via, + ctk_root=None if ctk_metadata is None else ctk_metadata.ctk_root, + ctk_version=None if ctk_metadata is None else ctk_metadata.ctk_version, + ctk_version_source=None if ctk_metadata is None else ctk_metadata.source, + ) + + +def _resolve_dynamic_lib_item(libname: str, loaded: LoadedDL) -> ResolvedItem: + if loaded.abs_path is None: + raise CompatibilityInsufficientMetadataError( + f"Could not determine an absolute path for dynamic library {libname!r}." + ) + desc = LIB_DESCRIPTORS[libname] + return _resolve_item( + name=libname, + kind="dynamic-lib", + packaged_with=desc.packaged_with, + abs_path=loaded.abs_path, + found_via=loaded.found_via, + ) + + +def _resolve_header_item(libname: str, located: LocatedHeaderDir) -> ResolvedItem: + if located.abs_path is None: + raise CompatibilityInsufficientMetadataError( + f"Could not determine an absolute path for header directory {libname!r}." + ) + desc = HEADER_DESCRIPTORS[libname] + metadata_abs_path = os.path.join(located.abs_path, desc.header_basename) + return _resolve_item( + name=libname, + kind="header-dir", + packaged_with=desc.packaged_with, + abs_path=metadata_abs_path, + found_via=located.found_via, + ) + + +def _resolve_static_lib_item(located: LocatedStaticLib) -> ResolvedItem: + packaged_with = _STATIC_LIBS_PACKAGED_WITH[located.name] + return _resolve_item( + name=located.name, + kind="static-lib", + packaged_with=packaged_with, + abs_path=located.abs_path, + found_via=located.found_via, + ) + + +def _resolve_bitcode_lib_item(located: LocatedBitcodeLib) -> ResolvedItem: + packaged_with = _BITCODE_LIBS_PACKAGED_WITH[located.name] + return _resolve_item( + name=located.name, + kind="bitcode-lib", + packaged_with=packaged_with, + abs_path=located.abs_path, + found_via=located.found_via, + ) + + +def _resolve_binary_item(utility_name: str, abs_path: str) -> ResolvedItem: + packaged_with = _BINARY_PACKAGED_WITH[utility_name] + return _resolve_item( + name=utility_name, + kind="binary", + packaged_with=packaged_with, + abs_path=abs_path, + found_via=None, + ) + + +def compatibility_check(driver_version: int, item1: ResolvedItem, item2: ResolvedItem) -> CompatibilityResult: + for item in (item1, item2): + if item.packaged_with != "ctk": + return CompatibilityResult( + status="insufficient_metadata", + message=( + "v1 compatibility checks only give definitive answers for " + f"packaged_with='ctk' items. {item.describe()} is packaged_with={item.packaged_with!r}." + ), + ) + if item.ctk_version is None or item.ctk_version_source is None: + return CompatibilityResult( + status="insufficient_metadata", + message=( + "v1 compatibility checks require either an enclosing CUDA Toolkit root " + "with version.json or wheel metadata that can be traced to an installed " + f"cuda-toolkit distribution. Could not determine the CTK version for {item.describe()}." + ), + ) + + assert item1.ctk_version is not None + assert item2.ctk_version is not None + + if item1.ctk_version != item2.ctk_version: + return CompatibilityResult( + status="incompatible", + message=( + f"{item1.describe()} resolves to CTK {item1.ctk_version}, while " + f"{item2.describe()} resolves to CTK {item2.ctk_version}. " + "v1 requires an exact CTK major.minor match." + ), + ) + + driver_major = _driver_major(driver_version) + if driver_major < item1.ctk_version.major: + return CompatibilityResult( + status="incompatible", + message=( + f"Driver version {driver_version} only supports CUDA major version {driver_major}, " + f"but {item1.describe()} requires CTK {item1.ctk_version}. " + "v1 requires driver_major >= ctk_major." + ), + ) + + return CompatibilityResult( + status="compatible", + message=( + f"{item1.describe()} and {item2.describe()} both resolve to CTK {item1.ctk_version}, " + f"and driver version {driver_version} satisfies the v1 driver guard rail." + ), + ) + + +def _query_driver_version() -> int: + loaded_cuda = _load_nvidia_dynamic_lib("cuda") + if loaded_cuda.abs_path is None: + raise CompatibilityCheckError('Could not determine an absolute path for the driver library "cuda".') + if IS_WINDOWS: + loader_cls_obj = vars(ctypes).get("WinDLL") + if loader_cls_obj is None: + raise CompatibilityCheckError("ctypes.WinDLL is unavailable on this platform.") + loader_cls = cast(Callable[[str], ctypes.CDLL], loader_cls_obj) + else: + loader_cls = ctypes.CDLL + driver_lib = loader_cls(loaded_cuda.abs_path) + cu_driver_get_version = driver_lib.cuDriverGetVersion + cu_driver_get_version.argtypes = [ctypes.POINTER(ctypes.c_int)] + cu_driver_get_version.restype = ctypes.c_int + version = ctypes.c_int() + status = cu_driver_get_version(ctypes.byref(version)) + if status != 0: + raise CompatibilityCheckError( + f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status})." + ) + return version.value + + +class WithCompatibilityChecks: + """Resolve CUDA artifacts while enforcing minimal v1 compatibility guard rails.""" + + def __init__( + self, + *, + ctk_major: ConstraintArg = None, + ctk_minor: ConstraintArg = None, + driver_version: DriverVersionArg = None, + ) -> None: + self._ctk_major_constraint = _coerce_constraint("ctk_major", ctk_major) + self._ctk_minor_constraint = _coerce_constraint("ctk_minor", ctk_minor) + self._driver_version = driver_version + self._resolved_items: list[ResolvedItem] = [] + + def _get_driver_version(self) -> int: + if self._driver_version is None: + self._driver_version = _query_driver_version() + return self._driver_version + + def _enforce_supported_packaging(self, item: ResolvedItem) -> None: + if item.packaged_with == "ctk": + return + raise CompatibilityInsufficientMetadataError( + "v1 compatibility checks only give definitive answers for " + f"packaged_with='ctk' items. {item.describe()} is packaged_with={item.packaged_with!r}." + ) + + def _enforce_ctk_metadata(self, item: ResolvedItem) -> None: + if item.ctk_version is not None and item.ctk_version_source is not None: + return + raise CompatibilityInsufficientMetadataError( + "v1 compatibility checks require either an enclosing CUDA Toolkit root " + "with version.json or wheel metadata that can be traced to an installed " + f"cuda-toolkit distribution. Could not determine the CTK version for {item.describe()}." + ) + + def _enforce_constraints(self, item: ResolvedItem) -> None: + assert item.ctk_version is not None + if self._ctk_major_constraint is not None and not self._ctk_major_constraint.matches(item.ctk_version.major): + raise CompatibilityCheckError( + f"{item.describe()} resolves to CTK {item.ctk_version}, which does not satisfy " + f"ctk_major{self._ctk_major_constraint}." + ) + if self._ctk_minor_constraint is not None and not self._ctk_minor_constraint.matches(item.ctk_version.minor): + raise CompatibilityCheckError( + f"{item.describe()} resolves to CTK {item.ctk_version}, which does not satisfy " + f"ctk_minor{self._ctk_minor_constraint}." + ) + + def _anchor_item(self) -> ResolvedItem | None: + if not self._resolved_items: + return None + return self._resolved_items[0] + + def _remember(self, item: ResolvedItem) -> None: + if item not in self._resolved_items: + self._resolved_items.append(item) + + def _register_and_check(self, item: ResolvedItem) -> None: + self._enforce_supported_packaging(item) + self._enforce_ctk_metadata(item) + self._enforce_constraints(item) + anchor = self._anchor_item() + if anchor is None: + anchor = item + compatibility_check(self._get_driver_version(), anchor, item).require_compatible() + self._remember(item) + + def load_nvidia_dynamic_lib(self, libname: str) -> LoadedDL: + """Load a CUDA dynamic library and reject v1-incompatible resolutions.""" + loaded = _load_nvidia_dynamic_lib(libname) + self._register_and_check(_resolve_dynamic_lib_item(libname, loaded)) + return loaded + + def locate_nvidia_header_directory(self, libname: str) -> LocatedHeaderDir | None: + """Locate a CUDA header directory and reject v1-incompatible resolutions.""" + located = _locate_nvidia_header_directory(libname) + if located is None: + return None + self._register_and_check(_resolve_header_item(libname, located)) + return located + + def find_nvidia_header_directory(self, libname: str) -> str | None: + """Locate a CUDA header directory and return only the path string.""" + located = self.locate_nvidia_header_directory(libname) + return None if located is None else located.abs_path + + def locate_static_lib(self, name: str) -> LocatedStaticLib: + """Locate a CUDA static library and reject v1-incompatible resolutions.""" + located = _locate_static_lib(name) + self._register_and_check(_resolve_static_lib_item(located)) + return located + + def find_static_lib(self, name: str) -> str: + """Locate a CUDA static library and return only the path string.""" + abs_path = self.locate_static_lib(name).abs_path + assert isinstance(abs_path, str) + return abs_path + + def locate_bitcode_lib(self, name: str) -> LocatedBitcodeLib: + """Locate a CUDA bitcode library and reject v1-incompatible resolutions.""" + located = _locate_bitcode_lib(name) + self._register_and_check(_resolve_bitcode_lib_item(located)) + return located + + def find_bitcode_lib(self, name: str) -> str: + """Locate a CUDA bitcode library and return only the path string.""" + abs_path = self.locate_bitcode_lib(name).abs_path + assert isinstance(abs_path, str) + return abs_path + + def find_nvidia_binary_utility(self, utility_name: str) -> str | None: + """Locate a CUDA binary utility and reject v1-incompatible resolutions.""" + abs_path = _find_nvidia_binary_utility(utility_name) + if abs_path is None: + return None + self._register_and_check(_resolve_binary_item(utility_name, abs_path)) + assert isinstance(abs_path, str) + return abs_path diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index e49478c09ec..1c58d4f41c9 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -18,6 +18,10 @@ CUDA bitcode and static libraries. get_cuda_path_or_home + WithCompatibilityChecks + CompatibilityCheckError + CompatibilityInsufficientMetadataError + SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib LoadedDL diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py new file mode 100644 index 00000000000..1b332fce592 --- /dev/null +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -0,0 +1,309 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import os +from pathlib import Path + +import pytest + +import cuda.pathfinder._compatibility as compatibility_module +from cuda.pathfinder import ( + BitcodeLibNotFoundError, + CompatibilityCheckError, + CompatibilityInsufficientMetadataError, + DynamicLibNotFoundError, + LoadedDL, + LocatedBitcodeLib, + LocatedHeaderDir, + LocatedStaticLib, + StaticLibNotFoundError, + WithCompatibilityChecks, +) + +STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_WITH_COMPATIBILITY_CHECKS_STRICTNESS", "see_what_works") +assert STRICTNESS in ("see_what_works", "all_must_work") + + +def _write_version_json(ctk_root: Path, toolkit_version: str) -> None: + ctk_root.mkdir(parents=True, exist_ok=True) + payload = {"cuda": {"version": toolkit_version}} + (ctk_root / "version.json").write_text(json.dumps(payload), encoding="utf-8") + + +def _touch(path: Path) -> str: + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + return str(path) + + +def _loaded_dl(abs_path: str, *, found_via: str = "CUDA_PATH") -> LoadedDL: + return LoadedDL( + abs_path=abs_path, + was_already_loaded_from_elsewhere=False, + _handle_uint=1, + found_via=found_via, + ) + + +def _located_static_lib(name: str, abs_path: str) -> LocatedStaticLib: + return LocatedStaticLib( + name=name, + abs_path=abs_path, + filename=os.path.basename(abs_path), + found_via="CUDA_PATH", + ) + + +def _located_bitcode_lib(name: str, abs_path: str) -> LocatedBitcodeLib: + return LocatedBitcodeLib( + name=name, + abs_path=abs_path, + filename=os.path.basename(abs_path), + found_via="CUDA_PATH", + ) + + +def test_load_dynamic_lib_then_find_headers_same_ctk_version(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + hdr_dir = ctk_root / "targets" / "x86_64-linux" / "include" + _touch(hdr_dir / "nvrtc.h") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + monkeypatch.setattr( + compatibility_module, + "_locate_nvidia_header_directory", + lambda _libname: LocatedHeaderDir(abs_path=str(hdr_dir), found_via="CUDA_PATH"), + ) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + + loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") + hdr_path = pfchecks.find_nvidia_header_directory("nvrtc") + + assert loaded.abs_path == lib_path + assert hdr_path == str(hdr_dir) + + +def test_exact_ctk_major_minor_match_is_required(monkeypatch, tmp_path): + lib_root = tmp_path / "cuda-12.8" + hdr_root = tmp_path / "cuda-12.9" + _write_version_json(lib_root, "12.8.20250303") + _write_version_json(hdr_root, "12.9.20250531") + + lib_path = _touch(lib_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + hdr_dir = hdr_root / "targets" / "x86_64-linux" / "include" + _touch(hdr_dir / "nvrtc.h") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + monkeypatch.setattr( + compatibility_module, + "_locate_nvidia_header_directory", + lambda _libname: LocatedHeaderDir(abs_path=str(hdr_dir), found_via="CUDA_PATH"), + ) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + with pytest.raises(CompatibilityCheckError, match="exact CTK major.minor match"): + pfchecks.find_nvidia_header_directory("nvrtc") + + +def test_driver_major_must_not_be_older_than_ctk_major(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-13.0" + _write_version_json(ctk_root, "13.0.20251003") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.13") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + pfchecks = WithCompatibilityChecks(driver_version=12080) + + with pytest.raises(CompatibilityCheckError, match="driver_major >= ctk_major"): + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + +def test_missing_version_json_raises_insufficient_metadata(monkeypatch, tmp_path): + lib_path = _touch(tmp_path / "no-version-json" / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + + with pytest.raises(CompatibilityInsufficientMetadataError, match="version.json"): + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + +def test_other_packaging_raises_insufficient_metadata(monkeypatch, tmp_path): + abs_path = _touch(tmp_path / "site-packages" / "nvidia" / "nvshmem" / "lib" / "libnvshmem_device.bc") + + monkeypatch.setattr( + compatibility_module, + "_locate_bitcode_lib", + lambda _name: _located_bitcode_lib("nvshmem_device", abs_path), + ) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + + with pytest.raises(CompatibilityInsufficientMetadataError, match="packaged_with='ctk'"): + pfchecks.find_bitcode_lib("nvshmem_device") + + +def test_constraints_accept_string_and_tuple_forms(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + pfchecks = WithCompatibilityChecks( + ctk_major=(">=", 12), + ctk_minor=">=9", + driver_version=13000, + ) + + loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") + + assert loaded.abs_path == lib_path + + +def test_constraint_failure_raises(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + pfchecks = WithCompatibilityChecks( + ctk_major=12, + ctk_minor="<9", + driver_version=13000, + ) + + with pytest.raises(CompatibilityCheckError, match="ctk_minor<9"): + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + +def test_static_bitcode_and_binary_methods_participate_in_checks(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + static_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libcudadevrt.a") + bitcode_path = _touch(ctk_root / "nvvm" / "libdevice" / "libdevice.10.bc") + binary_path = _touch(ctk_root / "bin" / "nvcc") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + monkeypatch.setattr( + compatibility_module, + "_locate_static_lib", + lambda _name: _located_static_lib("cudadevrt", static_path), + ) + monkeypatch.setattr( + compatibility_module, + "_locate_bitcode_lib", + lambda _name: _located_bitcode_lib("device", bitcode_path), + ) + monkeypatch.setattr( + compatibility_module, + "_find_nvidia_binary_utility", + lambda _utility_name: binary_path, + ) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + + pfchecks.load_nvidia_dynamic_lib("nvrtc") + assert pfchecks.find_static_lib("cudadevrt") == static_path + assert pfchecks.find_bitcode_lib("device") == bitcode_path + assert pfchecks.find_nvidia_binary_utility("nvcc") == binary_path + + +def test_wrapper_queries_driver_version_by_default(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + + query_calls: list[int] = [] + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + def fake_query_driver_version() -> int: + query_calls.append(1) + return 13000 + + monkeypatch.setattr(compatibility_module, "_query_driver_version", fake_query_driver_version) + + pfchecks = WithCompatibilityChecks() + + pfchecks.load_nvidia_dynamic_lib("nvrtc") + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + assert len(query_calls) == 1 + + +def test_find_nvidia_header_directory_returns_none_when_unresolved(monkeypatch): + monkeypatch.setattr( + compatibility_module, + "_locate_nvidia_header_directory", + lambda _libname: None, + ) + + pfchecks = WithCompatibilityChecks(driver_version=13000) + + assert pfchecks.find_nvidia_header_directory("nvrtc") is None + + +def test_real_wheel_ctk_items_are_compatible(info_summary_append): + pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_version=13000) + + try: + loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") + header_dir = pfchecks.find_nvidia_header_directory("nvrtc") + static_lib = pfchecks.find_static_lib("cudadevrt") + bitcode_lib = pfchecks.find_bitcode_lib("device") + nvcc = pfchecks.find_nvidia_binary_utility("nvcc") + except ( + CompatibilityCheckError, + CompatibilityInsufficientMetadataError, + DynamicLibNotFoundError, + StaticLibNotFoundError, + BitcodeLibNotFoundError, + ) as exc: + if STRICTNESS == "all_must_work": + raise + info_summary_append(f"real wheel check unavailable: {exc.__class__.__name__}: {exc}") + return + + info_summary_append(f"nvrtc={loaded.abs_path!r}") + info_summary_append(f"nvrtc_headers={header_dir!r}") + info_summary_append(f"cudadevrt={static_lib!r}") + info_summary_append(f"libdevice={bitcode_lib!r}") + info_summary_append(f"nvcc={nvcc!r}") + + assert isinstance(loaded.abs_path, str) + assert header_dir is not None + assert nvcc is not None + for path in (loaded.abs_path, header_dir, static_lib, bitcode_lib, nvcc): + assert "site-packages" in path + + +def test_real_wheel_component_version_does_not_override_ctk_line(info_summary_append): + pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_version=13000) + + try: + header_dir = pfchecks.find_nvidia_header_directory("cufft") + except (CompatibilityCheckError, CompatibilityInsufficientMetadataError) as exc: + if STRICTNESS == "all_must_work": + raise + info_summary_append(f"real cufft wheel check unavailable: {exc.__class__.__name__}: {exc}") + return + + if header_dir is None: + if STRICTNESS == "all_must_work": + raise AssertionError("Expected wheel-backed cufft headers to be discoverable.") + info_summary_append("real cufft wheel check unavailable: cufft headers not found") + return + + info_summary_append(f"cufft_headers={header_dir!r}") + assert "site-packages" in header_dir From 83c45fa974532f92c085b5a1c4aacf1c67fb2b2f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 16 Apr 2026 17:40:21 -0700 Subject: [PATCH 02/18] Update compatibility tests for system CTK paths Allow the real compatibility checks to pass when CTK artifacts come from a system install instead of site-packages, including cases where CUDA_PATH and CUDA_HOME are unset. Made-with: Cursor --- .../tests/test_with_compatibility_checks.py | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py index 1b332fce592..1b8ab1c6ba0 100644 --- a/cuda_pathfinder/tests/test_with_compatibility_checks.py +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -64,6 +64,30 @@ def _located_bitcode_lib(name: str, abs_path: str) -> LocatedBitcodeLib: ) +def _assert_real_ctk_backed_path(path: str) -> None: + norm_path = os.path.normpath(os.path.abspath(path)) + if "site-packages" in Path(norm_path).parts: + return + current = Path(norm_path) + if current.is_file(): + current = current.parent + for candidate in (current, *current.parents): + version_json_path = candidate / "version.json" + if version_json_path.is_file(): + return + for env_var in ("CUDA_PATH", "CUDA_HOME"): + ctk_root = os.environ.get(env_var) + if not ctk_root: + continue + norm_ctk_root = os.path.normpath(os.path.abspath(ctk_root)) + if os.path.commonpath((norm_path, norm_ctk_root)) == norm_ctk_root: + return + raise AssertionError( + "Expected a site-packages path, a path under a CTK root with version.json, " + f"or a path under CUDA_PATH/CUDA_HOME, got {path!r}" + ) + + def test_load_dynamic_lib_then_find_headers_same_ctk_version(monkeypatch, tmp_path): ctk_root = tmp_path / "cuda-12.9" _write_version_json(ctk_root, "12.9.20250531") @@ -272,7 +296,7 @@ def test_real_wheel_ctk_items_are_compatible(info_summary_append): ) as exc: if STRICTNESS == "all_must_work": raise - info_summary_append(f"real wheel check unavailable: {exc.__class__.__name__}: {exc}") + info_summary_append(f"real CTK check unavailable: {exc.__class__.__name__}: {exc}") return info_summary_append(f"nvrtc={loaded.abs_path!r}") @@ -285,7 +309,7 @@ def test_real_wheel_ctk_items_are_compatible(info_summary_append): assert header_dir is not None assert nvcc is not None for path in (loaded.abs_path, header_dir, static_lib, bitcode_lib, nvcc): - assert "site-packages" in path + _assert_real_ctk_backed_path(path) def test_real_wheel_component_version_does_not_override_ctk_line(info_summary_append): @@ -296,14 +320,14 @@ def test_real_wheel_component_version_does_not_override_ctk_line(info_summary_ap except (CompatibilityCheckError, CompatibilityInsufficientMetadataError) as exc: if STRICTNESS == "all_must_work": raise - info_summary_append(f"real cufft wheel check unavailable: {exc.__class__.__name__}: {exc}") + info_summary_append(f"real cufft CTK check unavailable: {exc.__class__.__name__}: {exc}") return if header_dir is None: if STRICTNESS == "all_must_work": - raise AssertionError("Expected wheel-backed cufft headers to be discoverable.") - info_summary_append("real cufft wheel check unavailable: cufft headers not found") + raise AssertionError("Expected CTK-backed cufft headers to be discoverable.") + info_summary_append("real cufft CTK check unavailable: cufft headers not found") return info_summary_append(f"cufft_headers={header_dir!r}") - assert "site-packages" in header_dir + _assert_real_ctk_backed_path(header_dir) From 7c6709c1ae5d4aa9718367aceca38b0948ba32b1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 10:45:36 -0700 Subject: [PATCH 03/18] Rename pathfinder compatibility checks module. Use a more explicit module name for the compatibility-check API and keep imports aligned with the renamed implementation. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/__init__.py | 6 +++--- .../{_compatibility.py => _compatibility_checks.py} | 0 cuda_pathfinder/tests/test_with_compatibility_checks.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename cuda_pathfinder/cuda/pathfinder/{_compatibility.py => _compatibility_checks.py} (100%) diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index e4451347535..3105523f8f5 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -11,13 +11,13 @@ find_nvidia_binary_utility as find_nvidia_binary_utility, ) from cuda.pathfinder._binaries.supported_nvidia_binaries import SUPPORTED_BINARIES as _SUPPORTED_BINARIES -from cuda.pathfinder._compatibility import ( +from cuda.pathfinder._compatibility_checks import ( CompatibilityCheckError as CompatibilityCheckError, ) -from cuda.pathfinder._compatibility import ( +from cuda.pathfinder._compatibility_checks import ( CompatibilityInsufficientMetadataError as CompatibilityInsufficientMetadataError, ) -from cuda.pathfinder._compatibility import ( +from cuda.pathfinder._compatibility_checks import ( WithCompatibilityChecks as WithCompatibilityChecks, ) from cuda.pathfinder._dynamic_libs.load_dl_common import ( diff --git a/cuda_pathfinder/cuda/pathfinder/_compatibility.py b/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py similarity index 100% rename from cuda_pathfinder/cuda/pathfinder/_compatibility.py rename to cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py index 1b8ab1c6ba0..c2e553bc8d0 100644 --- a/cuda_pathfinder/tests/test_with_compatibility_checks.py +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -7,7 +7,7 @@ import pytest -import cuda.pathfinder._compatibility as compatibility_module +import cuda.pathfinder._compatibility_checks as compatibility_module from cuda.pathfinder import ( BitcodeLibNotFoundError, CompatibilityCheckError, From a0de5f6d49ec54d15c5059f8d4223668dea66f38 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 11:04:16 -0700 Subject: [PATCH 04/18] Add a reusable pathfinder driver info helper. Break out CUDA driver version querying into a standalone internal utility so it can be reused independently from compatibility checks, and cover the ctypes loader paths with focused tests. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 36 ++++++ .../tests/test_utils_driver_info.py | 106 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py create mode 100644 cuda_pathfinder/tests/test_utils_driver_info.py diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py new file mode 100644 index 00000000000..e032e0bb2b1 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import ctypes +from collections.abc import Callable +from typing import cast + +from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( + load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib, +) +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + + +def _query_driver_version() -> int: + """Return the CUDA driver version from ``cuDriverGetVersion()``.""" + loaded_cuda = _load_nvidia_dynamic_lib("cuda") + if loaded_cuda.abs_path is None: + raise RuntimeError('Could not determine an absolute path for the driver library "cuda".') + if IS_WINDOWS: + loader_cls_obj = vars(ctypes).get("WinDLL") + if loader_cls_obj is None: + raise RuntimeError("ctypes.WinDLL is unavailable on this platform.") + loader_cls = cast(Callable[[str], ctypes.CDLL], loader_cls_obj) + else: + loader_cls = ctypes.CDLL + driver_lib = loader_cls(loaded_cuda.abs_path) + cu_driver_get_version = driver_lib.cuDriverGetVersion + cu_driver_get_version.argtypes = [ctypes.POINTER(ctypes.c_int)] + cu_driver_get_version.restype = ctypes.c_int + version = ctypes.c_int() + status = cu_driver_get_version(ctypes.byref(version)) + if status != 0: + raise RuntimeError(f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status}).") + return version.value diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py new file mode 100644 index 00000000000..7cbbc2d62bb --- /dev/null +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import ctypes + +import pytest + +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL +from cuda.pathfinder._utils import driver_info + + +class _FakeCuDriverGetVersion: + def __init__(self, *, status: int, version: int): + self.argtypes = None + self.restype = None + self._status = status + self._version = version + + def __call__(self, version_ptr) -> int: + ctypes.cast(version_ptr, ctypes.POINTER(ctypes.c_int)).contents.value = self._version + return self._status + + +class _FakeDriverLib: + def __init__(self, *, status: int, version: int): + self.cuDriverGetVersion = _FakeCuDriverGetVersion(status=status, version=version) + + +def _loaded_cuda(abs_path: str | None) -> LoadedDL: + return LoadedDL( + abs_path=abs_path, + was_already_loaded_from_elsewhere=False, + _handle_uint=0xBEEF, + found_via="system-search", + ) + + +def test_query_driver_version_uses_cdll_on_non_windows(monkeypatch): + fake_driver_lib = _FakeDriverLib(status=0, version=13020) + loaded_paths: list[str] = [] + + monkeypatch.setattr(driver_info, "IS_WINDOWS", False) + monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda("/usr/lib/libcuda.so.1")) + + def fake_cdll(abs_path: str): + loaded_paths.append(abs_path) + return fake_driver_lib + + monkeypatch.setattr(driver_info.ctypes, "CDLL", fake_cdll) + + assert driver_info._query_driver_version() == 13020 + assert loaded_paths == ["/usr/lib/libcuda.so.1"] + assert fake_driver_lib.cuDriverGetVersion.argtypes == [ctypes.POINTER(ctypes.c_int)] + assert fake_driver_lib.cuDriverGetVersion.restype is ctypes.c_int + + +def test_query_driver_version_uses_windll_on_windows(monkeypatch): + fake_driver_lib = _FakeDriverLib(status=0, version=12080) + loaded_paths: list[str] = [] + + monkeypatch.setattr(driver_info, "IS_WINDOWS", True) + monkeypatch.setattr( + driver_info, + "_load_nvidia_dynamic_lib", + lambda _libname: _loaded_cuda(r"C:\Windows\System32\nvcuda.dll"), + ) + + def fake_windll(abs_path: str): + loaded_paths.append(abs_path) + return fake_driver_lib + + monkeypatch.setattr(driver_info.ctypes, "WinDLL", fake_windll, raising=False) + + assert driver_info._query_driver_version() == 12080 + assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"] + + +def test_query_driver_version_raises_when_cuda_abs_path_is_missing(monkeypatch): + monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda(None)) + + with pytest.raises(RuntimeError, match='Could not determine an absolute path for the driver library "cuda"'): + driver_info._query_driver_version() + + +def test_query_driver_version_raises_when_windll_is_unavailable(monkeypatch): + monkeypatch.setattr(driver_info, "IS_WINDOWS", True) + monkeypatch.setattr( + driver_info, + "_load_nvidia_dynamic_lib", + lambda _libname: _loaded_cuda(r"C:\Windows\System32\nvcuda.dll"), + ) + monkeypatch.delattr(driver_info.ctypes, "WinDLL", raising=False) + + with pytest.raises(RuntimeError, match="ctypes.WinDLL is unavailable on this platform"): + driver_info._query_driver_version() + + +def test_query_driver_version_raises_when_cuda_call_fails(monkeypatch): + fake_driver_lib = _FakeDriverLib(status=1, version=0) + + monkeypatch.setattr(driver_info, "IS_WINDOWS", False) + monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda("/usr/lib/libcuda.so.1")) + monkeypatch.setattr(driver_info.ctypes, "CDLL", lambda _abs_path: fake_driver_lib) + + with pytest.raises(RuntimeError, match=r"cuDriverGetVersion\(\) \(status=1\)"): + driver_info._query_driver_version() From 91d38ffa434b57aeec2c9f126ea501fefb72e9be Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 11:36:53 -0700 Subject: [PATCH 05/18] Refine pathfinder driver info loader checks. Treat the Windows WinDLL path as the normal runtime case and keep the focused tests aligned with the stricter driver-loader invariants. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 10 +++------ .../tests/test_utils_driver_info.py | 22 +------------------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index e032e0bb2b1..a5274f1c5fe 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -5,7 +5,6 @@ import ctypes from collections.abc import Callable -from typing import cast from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib, @@ -16,13 +15,10 @@ def _query_driver_version() -> int: """Return the CUDA driver version from ``cuDriverGetVersion()``.""" loaded_cuda = _load_nvidia_dynamic_lib("cuda") - if loaded_cuda.abs_path is None: - raise RuntimeError('Could not determine an absolute path for the driver library "cuda".') if IS_WINDOWS: - loader_cls_obj = vars(ctypes).get("WinDLL") - if loader_cls_obj is None: - raise RuntimeError("ctypes.WinDLL is unavailable on this platform.") - loader_cls = cast(Callable[[str], ctypes.CDLL], loader_cls_obj) + # `ctypes.WinDLL` exists on Windows at runtime. The ignore is only for + # Linux mypy runs, where the platform stubs do not define that attribute. + loader_cls: Callable[[str], ctypes.CDLL] = ctypes.WinDLL # type: ignore[attr-defined] else: loader_cls = ctypes.CDLL driver_lib = loader_cls(loaded_cuda.abs_path) diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 7cbbc2d62bb..6b6e7faeec3 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -26,7 +26,7 @@ def __init__(self, *, status: int, version: int): self.cuDriverGetVersion = _FakeCuDriverGetVersion(status=status, version=version) -def _loaded_cuda(abs_path: str | None) -> LoadedDL: +def _loaded_cuda(abs_path: str) -> LoadedDL: return LoadedDL( abs_path=abs_path, was_already_loaded_from_elsewhere=False, @@ -75,26 +75,6 @@ def fake_windll(abs_path: str): assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"] -def test_query_driver_version_raises_when_cuda_abs_path_is_missing(monkeypatch): - monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda(None)) - - with pytest.raises(RuntimeError, match='Could not determine an absolute path for the driver library "cuda"'): - driver_info._query_driver_version() - - -def test_query_driver_version_raises_when_windll_is_unavailable(monkeypatch): - monkeypatch.setattr(driver_info, "IS_WINDOWS", True) - monkeypatch.setattr( - driver_info, - "_load_nvidia_dynamic_lib", - lambda _libname: _loaded_cuda(r"C:\Windows\System32\nvcuda.dll"), - ) - monkeypatch.delattr(driver_info.ctypes, "WinDLL", raising=False) - - with pytest.raises(RuntimeError, match="ctypes.WinDLL is unavailable on this platform"): - driver_info._query_driver_version() - - def test_query_driver_version_raises_when_cuda_call_fails(monkeypatch): fake_driver_lib = _FakeDriverLib(status=1, version=0) From 14450c1d788411ecabf4e37a0e05e5b347cbbeaa Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 11:45:29 -0700 Subject: [PATCH 06/18] Add parsed pathfinder driver version metadata. Wrap the encoded cuDriverGetVersion() result in a DriverVersion dataclass so callers can use major and minor fields directly while retaining a low-level integer helper for loader-focused tests. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 22 +++++++++++++++++-- .../tests/test_utils_driver_info.py | 18 +++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index a5274f1c5fe..bbdb5643ea6 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -5,6 +5,7 @@ import ctypes from collections.abc import Callable +from dataclasses import dataclass from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib, @@ -12,8 +13,25 @@ from cuda.pathfinder._utils.platform_aware import IS_WINDOWS -def _query_driver_version() -> int: - """Return the CUDA driver version from ``cuDriverGetVersion()``.""" +@dataclass(frozen=True, slots=True) +class DriverVersion: + encoded: int + major: int + minor: int + + +def query_driver_version() -> DriverVersion: + """Return the CUDA driver version parsed into its major/minor components.""" + encoded = _query_driver_version_int() + return DriverVersion( + encoded=encoded, + major=encoded // 1000, + minor=(encoded % 1000) // 10, + ) + + +def _query_driver_version_int() -> int: + """Return the encoded CUDA driver version from ``cuDriverGetVersion()``.""" loaded_cuda = _load_nvidia_dynamic_lib("cuda") if IS_WINDOWS: # `ctypes.WinDLL` exists on Windows at runtime. The ignore is only for diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 6b6e7faeec3..f7c9c2981b6 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -48,7 +48,7 @@ def fake_cdll(abs_path: str): monkeypatch.setattr(driver_info.ctypes, "CDLL", fake_cdll) - assert driver_info._query_driver_version() == 13020 + assert driver_info._query_driver_version_int() == 13020 assert loaded_paths == ["/usr/lib/libcuda.so.1"] assert fake_driver_lib.cuDriverGetVersion.argtypes == [ctypes.POINTER(ctypes.c_int)] assert fake_driver_lib.cuDriverGetVersion.restype is ctypes.c_int @@ -71,11 +71,21 @@ def fake_windll(abs_path: str): monkeypatch.setattr(driver_info.ctypes, "WinDLL", fake_windll, raising=False) - assert driver_info._query_driver_version() == 12080 + assert driver_info._query_driver_version_int() == 12080 assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"] -def test_query_driver_version_raises_when_cuda_call_fails(monkeypatch): +def test_query_driver_version_returns_parsed_dataclass(monkeypatch): + monkeypatch.setattr(driver_info, "_query_driver_version_int", lambda: 12080) + + assert driver_info.query_driver_version() == driver_info.DriverVersion( + encoded=12080, + major=12, + minor=8, + ) + + +def test_query_driver_version_int_raises_when_cuda_call_fails(monkeypatch): fake_driver_lib = _FakeDriverLib(status=1, version=0) monkeypatch.setattr(driver_info, "IS_WINDOWS", False) @@ -83,4 +93,4 @@ def test_query_driver_version_raises_when_cuda_call_fails(monkeypatch): monkeypatch.setattr(driver_info.ctypes, "CDLL", lambda _abs_path: fake_driver_lib) with pytest.raises(RuntimeError, match=r"cuDriverGetVersion\(\) \(status=1\)"): - driver_info._query_driver_version() + driver_info._query_driver_version_int() From 329d952ad1821bd68b9d150606f07b41b6e362b7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 12:06:26 -0700 Subject: [PATCH 07/18] Add a real pathfinder driver version test. Cover query_driver_version() alongside the driver library loading tests and reuse the existing strictness mode so host-specific failures still surface cleanly in all_must_work mode. Made-with: Cursor --- .../tests/test_driver_lib_loading.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cuda_pathfinder/tests/test_driver_lib_loading.py b/cuda_pathfinder/tests/test_driver_lib_loading.py index bf62a17d703..36ba376764d 100644 --- a/cuda_pathfinder/tests/test_driver_lib_loading.py +++ b/cuda_pathfinder/tests/test_driver_lib_loading.py @@ -25,6 +25,7 @@ _load_lib_no_cache, ) from cuda.pathfinder._dynamic_libs.subprocess_protocol import STATUS_NOT_FOUND, parse_dynamic_lib_subprocess_payload +from cuda.pathfinder._utils import driver_info from cuda.pathfinder._utils.platform_aware import IS_WINDOWS, quote_for_shell STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS", "see_what_works") @@ -157,3 +158,21 @@ def raise_child_process_failed(): assert abs_path is not None info_summary_append(f"abs_path={quote_for_shell(abs_path)}") assert os.path.isfile(abs_path) + + +def test_real_query_driver_version(info_summary_append): + driver_info._load_nvidia_dynamic_lib.cache_clear() + try: + version = driver_info.query_driver_version() + except Exception as exc: + if STRICTNESS == "all_must_work": + raise + info_summary_append(f"driver version unavailable: {exc.__class__.__name__}: {exc}") + return + finally: + driver_info._load_nvidia_dynamic_lib.cache_clear() + + info_summary_append(f"driver_version={version.major}.{version.minor} (encoded={version.encoded})") + assert version.encoded > 0 + assert version.major == version.encoded // 1000 + assert version.minor == (version.encoded % 1000) // 10 From 0bcbd23e4f335e690aec1440b66831dfb6fe4e43 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 12:22:28 -0700 Subject: [PATCH 08/18] Reduce redundant pathfinder driver info mocks. Drop the non-Windows loader mock now that a real driver-version test covers the Linux success path, while keeping the Windows branch and failure-path unit coverage. Made-with: Cursor --- .../tests/test_utils_driver_info.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index f7c9c2981b6..37ebc6cd4c9 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -35,25 +35,6 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) -def test_query_driver_version_uses_cdll_on_non_windows(monkeypatch): - fake_driver_lib = _FakeDriverLib(status=0, version=13020) - loaded_paths: list[str] = [] - - monkeypatch.setattr(driver_info, "IS_WINDOWS", False) - monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda("/usr/lib/libcuda.so.1")) - - def fake_cdll(abs_path: str): - loaded_paths.append(abs_path) - return fake_driver_lib - - monkeypatch.setattr(driver_info.ctypes, "CDLL", fake_cdll) - - assert driver_info._query_driver_version_int() == 13020 - assert loaded_paths == ["/usr/lib/libcuda.so.1"] - assert fake_driver_lib.cuDriverGetVersion.argtypes == [ctypes.POINTER(ctypes.c_int)] - assert fake_driver_lib.cuDriverGetVersion.restype is ctypes.c_int - - def test_query_driver_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] From f72c96385be8a56fd875ec745be8e6492cf71118 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 12:42:04 -0700 Subject: [PATCH 09/18] Rename the pathfinder CUDA driver version dataclass. Use DriverCudaVersion for clearer pairing with the planned release-version type while keeping the existing driver info API behavior unchanged. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py | 6 +++--- cuda_pathfinder/tests/test_utils_driver_info.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index bbdb5643ea6..d83f180d677 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -14,16 +14,16 @@ @dataclass(frozen=True, slots=True) -class DriverVersion: +class DriverCudaVersion: encoded: int major: int minor: int -def query_driver_version() -> DriverVersion: +def query_driver_version() -> DriverCudaVersion: """Return the CUDA driver version parsed into its major/minor components.""" encoded = _query_driver_version_int() - return DriverVersion( + return DriverCudaVersion( encoded=encoded, major=encoded // 1000, minor=(encoded % 1000) // 10, diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 37ebc6cd4c9..21855339736 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -59,7 +59,7 @@ def fake_windll(abs_path: str): def test_query_driver_version_returns_parsed_dataclass(monkeypatch): monkeypatch.setattr(driver_info, "_query_driver_version_int", lambda: 12080) - assert driver_info.query_driver_version() == driver_info.DriverVersion( + assert driver_info.query_driver_version() == driver_info.DriverCudaVersion( encoded=12080, major=12, minor=8, From 4eade17f56cc9a45ffa5ef20156553b8075a258d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 13:46:38 -0700 Subject: [PATCH 10/18] Add a pathfinder NVML driver release version helper. Query nvmlSystemGetDriverVersion() through pathfinder's driver library loading path and add a minimal real test so the implementation is preserved as a future reference. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 44 +++++++++++++++++++ .../tests/test_utils_driver_info.py | 14 ++++++ 2 files changed, 58 insertions(+) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index d83f180d677..3d85e7349d0 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -12,6 +12,9 @@ ) from cuda.pathfinder._utils.platform_aware import IS_WINDOWS +_NVML_SUCCESS = 0 +_NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH = 80 + @dataclass(frozen=True, slots=True) class DriverCudaVersion: @@ -48,3 +51,44 @@ def _query_driver_version_int() -> int: if status != 0: raise RuntimeError(f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status}).") return version.value + + +def _query_driver_release_version_text() -> str: + """Return the graphics driver release version from ``nvmlSystemGetDriverVersion()``.""" + loaded_nvml = _load_nvidia_dynamic_lib("nvml") + nvml_lib = ctypes.CDLL(loaded_nvml.abs_path) + + nvml_init_v2 = nvml_lib.nvmlInit_v2 + nvml_init_v2.argtypes = [] + nvml_init_v2.restype = ctypes.c_int + + nvml_system_get_driver_version = nvml_lib.nvmlSystemGetDriverVersion + nvml_system_get_driver_version.argtypes = [ctypes.POINTER(ctypes.c_char), ctypes.c_uint] + nvml_system_get_driver_version.restype = ctypes.c_int + + nvml_shutdown = nvml_lib.nvmlShutdown + nvml_shutdown.argtypes = [] + nvml_shutdown.restype = ctypes.c_int + + init_status = nvml_init_v2() + if init_status != _NVML_SUCCESS: + raise RuntimeError(f"Failed to initialize NVML via nvmlInit_v2() (status={init_status}).") + + try: + version_buffer = ctypes.create_string_buffer(_NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH) + status = nvml_system_get_driver_version(version_buffer, _NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH) + if status != _NVML_SUCCESS: + raise RuntimeError( + f"Failed to query driver release version via nvmlSystemGetDriverVersion() (status={status})." + ) + release_version = version_buffer.value.decode() + except BaseException as exc: + shutdown_status = nvml_shutdown() + if shutdown_status != _NVML_SUCCESS: + raise RuntimeError(f"Failed to shut down NVML via nvmlShutdown() (status={shutdown_status}).") from exc + raise + + shutdown_status = nvml_shutdown() + if shutdown_status != _NVML_SUCCESS: + raise RuntimeError(f"Failed to shut down NVML via nvmlShutdown() (status={shutdown_status}).") + return release_version diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 21855339736..e8eb0e6de42 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -35,6 +35,20 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) +def test_query_driver_release_version_text(): + driver_info._load_nvidia_dynamic_lib.cache_clear() + try: + release_version = driver_info._query_driver_release_version_text() + finally: + driver_info._load_nvidia_dynamic_lib.cache_clear() + + components = tuple(int(component) for component in release_version.split(".")) + assert len(components) in (2, 3) + assert 400 <= components[0] < 1000 + for component in components[1:]: + assert component >= 0 + + def test_query_driver_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] From 32e814fedd518ac266482dde2037ebf587cad64d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 13:46:52 -0700 Subject: [PATCH 11/18] Revert the pathfinder NVML driver release version helper. Step back from the exploratory NVML-based release-version query for now because it adds non-trivial complexity and a new dependency surface without a current pathfinder need, while keeping the reference implementation in history if we need it later. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 44 ------------------- .../tests/test_utils_driver_info.py | 14 ------ 2 files changed, 58 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index 3d85e7349d0..d83f180d677 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -12,9 +12,6 @@ ) from cuda.pathfinder._utils.platform_aware import IS_WINDOWS -_NVML_SUCCESS = 0 -_NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH = 80 - @dataclass(frozen=True, slots=True) class DriverCudaVersion: @@ -51,44 +48,3 @@ def _query_driver_version_int() -> int: if status != 0: raise RuntimeError(f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status}).") return version.value - - -def _query_driver_release_version_text() -> str: - """Return the graphics driver release version from ``nvmlSystemGetDriverVersion()``.""" - loaded_nvml = _load_nvidia_dynamic_lib("nvml") - nvml_lib = ctypes.CDLL(loaded_nvml.abs_path) - - nvml_init_v2 = nvml_lib.nvmlInit_v2 - nvml_init_v2.argtypes = [] - nvml_init_v2.restype = ctypes.c_int - - nvml_system_get_driver_version = nvml_lib.nvmlSystemGetDriverVersion - nvml_system_get_driver_version.argtypes = [ctypes.POINTER(ctypes.c_char), ctypes.c_uint] - nvml_system_get_driver_version.restype = ctypes.c_int - - nvml_shutdown = nvml_lib.nvmlShutdown - nvml_shutdown.argtypes = [] - nvml_shutdown.restype = ctypes.c_int - - init_status = nvml_init_v2() - if init_status != _NVML_SUCCESS: - raise RuntimeError(f"Failed to initialize NVML via nvmlInit_v2() (status={init_status}).") - - try: - version_buffer = ctypes.create_string_buffer(_NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH) - status = nvml_system_get_driver_version(version_buffer, _NVML_SYSTEM_DRIVER_VERSION_BUFFER_LENGTH) - if status != _NVML_SUCCESS: - raise RuntimeError( - f"Failed to query driver release version via nvmlSystemGetDriverVersion() (status={status})." - ) - release_version = version_buffer.value.decode() - except BaseException as exc: - shutdown_status = nvml_shutdown() - if shutdown_status != _NVML_SUCCESS: - raise RuntimeError(f"Failed to shut down NVML via nvmlShutdown() (status={shutdown_status}).") from exc - raise - - shutdown_status = nvml_shutdown() - if shutdown_status != _NVML_SUCCESS: - raise RuntimeError(f"Failed to shut down NVML via nvmlShutdown() (status={shutdown_status}).") - return release_version diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index e8eb0e6de42..21855339736 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -35,20 +35,6 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) -def test_query_driver_release_version_text(): - driver_info._load_nvidia_dynamic_lib.cache_clear() - try: - release_version = driver_info._query_driver_release_version_text() - finally: - driver_info._load_nvidia_dynamic_lib.cache_clear() - - components = tuple(int(component) for component in release_version.split(".")) - assert len(components) in (2, 3) - assert 400 <= components[0] < 1000 - for component in components[1:]: - assert component >= 0 - - def test_query_driver_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] From 1369c175780e620ee2adae2f9755c825ca31d51d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 13:54:26 -0700 Subject: [PATCH 12/18] Clarify the pathfinder CUDA driver version naming. Document that DriverCudaVersion matches the CUDA Version shown by nvidia-smi rather than the graphics driver release, so the dataclass name reads clearly in context. Made-with: Cursor --- .../cuda/pathfinder/_utils/driver_info.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index d83f180d677..c7de1a7b36e 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -15,6 +15,24 @@ @dataclass(frozen=True, slots=True) class DriverCudaVersion: + """ + CUDA-facing driver version reported by ``cuDriverGetVersion()``. + + The name ``DriverCudaVersion`` is intentionally specific: this dataclass + models the version shown as ``CUDA Version`` in ``nvidia-smi``, not the + graphics driver release shown as ``Driver Version``. + + Example ``nvidia-smi`` output:: + + +---------------------------------------------------------------------+ + | NVIDIA-SMI 595.58.03 Driver Version: 595.58.03 CUDA Version: 13.2 | + +---------------------------------------------------------------------+ + + For the example above, ``DriverCudaVersion(encoded=13020, major=13, + minor=2)`` corresponds to ``CUDA Version: 13.2``. It does not correspond + to ``Driver Version: 595.58.03``. + """ + encoded: int major: int minor: int From 772451b13cb61e2ca48cf2ff96bb5f0c4a36c2c2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 14:16:24 -0700 Subject: [PATCH 13/18] Finalize the pathfinder CUDA driver version query API. Expose DriverCudaVersion, QueryDriverCudaVersionError, and query_driver_cuda_version publicly, and align the internal naming, caching, docs, and test coverage around the CUDA-specific driver version query. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/__init__.py | 3 ++ .../cuda/pathfinder/_utils/driver_info.py | 25 ++++++++---- cuda_pathfinder/docs/source/api.rst | 4 ++ .../tests/test_driver_lib_loading.py | 8 ++-- .../tests/test_utils_driver_info.py | 38 +++++++++++++++---- 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index dc818dfd08f..89becb1834f 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -59,6 +59,9 @@ from cuda.pathfinder._static_libs.find_static_lib import ( locate_static_lib as locate_static_lib, ) +from cuda.pathfinder._utils.driver_info import DriverCudaVersion as DriverCudaVersion +from cuda.pathfinder._utils.driver_info import QueryDriverCudaVersionError as QueryDriverCudaVersionError +from cuda.pathfinder._utils.driver_info import query_driver_cuda_version as query_driver_cuda_version from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home as get_cuda_path_or_home from cuda.pathfinder._version import __version__ # isort: skip diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py index c7de1a7b36e..141b3465d49 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py @@ -4,6 +4,7 @@ from __future__ import annotations import ctypes +import functools from collections.abc import Callable from dataclasses import dataclass @@ -13,6 +14,10 @@ from cuda.pathfinder._utils.platform_aware import IS_WINDOWS +class QueryDriverCudaVersionError(RuntimeError): + """Raised when ``query_driver_cuda_version()`` cannot determine the CUDA driver version.""" + + @dataclass(frozen=True, slots=True) class DriverCudaVersion: """ @@ -38,17 +43,21 @@ class DriverCudaVersion: minor: int -def query_driver_version() -> DriverCudaVersion: +@functools.cache +def query_driver_cuda_version() -> DriverCudaVersion: """Return the CUDA driver version parsed into its major/minor components.""" - encoded = _query_driver_version_int() - return DriverCudaVersion( - encoded=encoded, - major=encoded // 1000, - minor=(encoded % 1000) // 10, - ) + try: + encoded = _query_driver_cuda_version_int() + return DriverCudaVersion( + encoded=encoded, + major=encoded // 1000, + minor=(encoded % 1000) // 10, + ) + except Exception as exc: + raise QueryDriverCudaVersionError("Failed to query the CUDA driver version.") from exc -def _query_driver_version_int() -> int: +def _query_driver_cuda_version_int() -> int: """Return the encoded CUDA driver version from ``cuDriverGetVersion()``.""" loaded_cuda = _load_nvidia_dynamic_lib("cuda") if IS_WINDOWS: diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index e49478c09ec..7fe9ea54e9d 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -18,6 +18,10 @@ CUDA bitcode and static libraries. get_cuda_path_or_home + DriverCudaVersion + QueryDriverCudaVersionError + query_driver_cuda_version + SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib LoadedDL diff --git a/cuda_pathfinder/tests/test_driver_lib_loading.py b/cuda_pathfinder/tests/test_driver_lib_loading.py index 36ba376764d..b97453c9b5a 100644 --- a/cuda_pathfinder/tests/test_driver_lib_loading.py +++ b/cuda_pathfinder/tests/test_driver_lib_loading.py @@ -160,17 +160,19 @@ def raise_child_process_failed(): assert os.path.isfile(abs_path) -def test_real_query_driver_version(info_summary_append): +def test_real_query_driver_cuda_version(info_summary_append): driver_info._load_nvidia_dynamic_lib.cache_clear() + driver_info.query_driver_cuda_version.cache_clear() try: - version = driver_info.query_driver_version() - except Exception as exc: + version = driver_info.query_driver_cuda_version() + except driver_info.QueryDriverCudaVersionError as exc: if STRICTNESS == "all_must_work": raise info_summary_append(f"driver version unavailable: {exc.__class__.__name__}: {exc}") return finally: driver_info._load_nvidia_dynamic_lib.cache_clear() + driver_info.query_driver_cuda_version.cache_clear() info_summary_append(f"driver_version={version.major}.{version.minor} (encoded={version.encoded})") assert version.encoded > 0 diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 21855339736..21948dadafe 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -9,6 +9,13 @@ from cuda.pathfinder._utils import driver_info +@pytest.fixture(autouse=True) +def _clear_driver_cuda_version_query_cache(): + driver_info.query_driver_cuda_version.cache_clear() + yield + driver_info.query_driver_cuda_version.cache_clear() + + class _FakeCuDriverGetVersion: def __init__(self, *, status: int, version: int): self.argtypes = None @@ -35,7 +42,7 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) -def test_query_driver_version_uses_windll_on_windows(monkeypatch): +def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] @@ -52,21 +59,38 @@ def fake_windll(abs_path: str): monkeypatch.setattr(driver_info.ctypes, "WinDLL", fake_windll, raising=False) - assert driver_info._query_driver_version_int() == 12080 + assert driver_info._query_driver_cuda_version_int() == 12080 assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"] -def test_query_driver_version_returns_parsed_dataclass(monkeypatch): - monkeypatch.setattr(driver_info, "_query_driver_version_int", lambda: 12080) +def test_query_driver_cuda_version_returns_parsed_dataclass(monkeypatch): + monkeypatch.setattr(driver_info, "_query_driver_cuda_version_int", lambda: 12080) - assert driver_info.query_driver_version() == driver_info.DriverCudaVersion( + assert driver_info.query_driver_cuda_version() == driver_info.DriverCudaVersion( encoded=12080, major=12, minor=8, ) -def test_query_driver_version_int_raises_when_cuda_call_fails(monkeypatch): +def test_query_driver_cuda_version_wraps_internal_failures(monkeypatch): + root_cause = RuntimeError("low-level query failed") + + def fail_query_driver_cuda_version_int() -> int: + raise root_cause + + monkeypatch.setattr(driver_info, "_query_driver_cuda_version_int", fail_query_driver_cuda_version_int) + + with pytest.raises( + driver_info.QueryDriverCudaVersionError, + match="Failed to query the CUDA driver version", + ) as exc_info: + driver_info.query_driver_cuda_version() + + assert exc_info.value.__cause__ is root_cause + + +def test_query_driver_cuda_version_int_raises_when_cuda_call_fails(monkeypatch): fake_driver_lib = _FakeDriverLib(status=1, version=0) monkeypatch.setattr(driver_info, "IS_WINDOWS", False) @@ -74,4 +98,4 @@ def test_query_driver_version_int_raises_when_cuda_call_fails(monkeypatch): monkeypatch.setattr(driver_info.ctypes, "CDLL", lambda _abs_path: fake_driver_lib) with pytest.raises(RuntimeError, match=r"cuDriverGetVersion\(\) \(status=1\)"): - driver_info._query_driver_version_int() + driver_info._query_driver_cuda_version_int() From 6ec081c75f64855b67914fe48744ecd02456dbd5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 14:32:18 -0700 Subject: [PATCH 14/18] Add a public pathfinder driver info regression test. Protect the new top-level driver-info re-exports so internal-only test coverage does not miss a broken `cuda.pathfinder` plumbing layer. Made-with: Cursor --- cuda_pathfinder/tests/test_utils_driver_info.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 21948dadafe..9ebb98e4fb6 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -5,6 +5,15 @@ import pytest +from cuda.pathfinder import ( + DriverCudaVersion as PublicDriverCudaVersion, +) +from cuda.pathfinder import ( + QueryDriverCudaVersionError as PublicQueryDriverCudaVersionError, +) +from cuda.pathfinder import ( + query_driver_cuda_version as public_query_driver_cuda_version, +) from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL from cuda.pathfinder._utils import driver_info @@ -42,6 +51,12 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) +def test_driver_cuda_version_public_api_exports(): + assert PublicDriverCudaVersion is driver_info.DriverCudaVersion + assert PublicQueryDriverCudaVersionError is driver_info.QueryDriverCudaVersionError + assert public_query_driver_cuda_version is driver_info.query_driver_cuda_version + + def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] From 0eab1ab4d887c76d53ebb07652379503a774f556 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 21 Apr 2026 11:32:09 -0700 Subject: [PATCH 15/18] Use the shared pathfinder driver query in compatibility checks. Route `WithCompatibilityChecks` through `query_driver_cuda_version()` so the wrapper reuses the public driver-info helper and preserves a compatibility-layer error when the implicit driver query fails. Made-with: Cursor --- .../cuda/pathfinder/_compatibility_checks.py | 39 ++++++------------- .../tests/test_with_compatibility_checks.py | 31 +++++++++++++-- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py b/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py index ec185bec444..af3a4ed2131 100644 --- a/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py +++ b/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py @@ -3,13 +3,12 @@ from __future__ import annotations -import ctypes import functools import importlib.metadata import json import os import re -from collections.abc import Callable, Mapping +from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path from typing import TypeAlias, cast @@ -42,7 +41,10 @@ from cuda.pathfinder._static_libs.find_static_lib import ( locate_static_lib as _locate_static_lib, ) -from cuda.pathfinder._utils.platform_aware import IS_WINDOWS +from cuda.pathfinder._utils.driver_info import ( + QueryDriverCudaVersionError, + query_driver_cuda_version, +) ItemKind: TypeAlias = str PackagedWith: TypeAlias = str @@ -429,30 +431,6 @@ def compatibility_check(driver_version: int, item1: ResolvedItem, item2: Resolve ) -def _query_driver_version() -> int: - loaded_cuda = _load_nvidia_dynamic_lib("cuda") - if loaded_cuda.abs_path is None: - raise CompatibilityCheckError('Could not determine an absolute path for the driver library "cuda".') - if IS_WINDOWS: - loader_cls_obj = vars(ctypes).get("WinDLL") - if loader_cls_obj is None: - raise CompatibilityCheckError("ctypes.WinDLL is unavailable on this platform.") - loader_cls = cast(Callable[[str], ctypes.CDLL], loader_cls_obj) - else: - loader_cls = ctypes.CDLL - driver_lib = loader_cls(loaded_cuda.abs_path) - cu_driver_get_version = driver_lib.cuDriverGetVersion - cu_driver_get_version.argtypes = [ctypes.POINTER(ctypes.c_int)] - cu_driver_get_version.restype = ctypes.c_int - version = ctypes.c_int() - status = cu_driver_get_version(ctypes.byref(version)) - if status != 0: - raise CompatibilityCheckError( - f"Failed to query CUDA driver version via cuDriverGetVersion() (status={status})." - ) - return version.value - - class WithCompatibilityChecks: """Resolve CUDA artifacts while enforcing minimal v1 compatibility guard rails.""" @@ -470,7 +448,12 @@ def __init__( def _get_driver_version(self) -> int: if self._driver_version is None: - self._driver_version = _query_driver_version() + try: + self._driver_version = query_driver_cuda_version().encoded + except QueryDriverCudaVersionError as exc: + raise CompatibilityCheckError( + "Failed to query the CUDA driver version needed for compatibility checks." + ) from exc return self._driver_version def _enforce_supported_packaging(self, item: ResolvedItem) -> None: diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py index c2e553bc8d0..d51427c2e26 100644 --- a/cuda_pathfinder/tests/test_with_compatibility_checks.py +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -12,11 +12,13 @@ BitcodeLibNotFoundError, CompatibilityCheckError, CompatibilityInsufficientMetadataError, + DriverCudaVersion, DynamicLibNotFoundError, LoadedDL, LocatedBitcodeLib, LocatedHeaderDir, LocatedStaticLib, + QueryDriverCudaVersionError, StaticLibNotFoundError, WithCompatibilityChecks, ) @@ -252,11 +254,11 @@ def test_wrapper_queries_driver_version_by_default(monkeypatch, tmp_path): monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) - def fake_query_driver_version() -> int: + def fake_query_driver_cuda_version() -> DriverCudaVersion: query_calls.append(1) - return 13000 + return DriverCudaVersion(encoded=13000, major=13, minor=0) - monkeypatch.setattr(compatibility_module, "_query_driver_version", fake_query_driver_version) + monkeypatch.setattr(compatibility_module, "query_driver_cuda_version", fake_query_driver_cuda_version) pfchecks = WithCompatibilityChecks() @@ -266,6 +268,29 @@ def fake_query_driver_version() -> int: assert len(query_calls) == 1 +def test_wrapper_wraps_driver_query_failures(monkeypatch, tmp_path): + ctk_root = tmp_path / "cuda-12.9" + _write_version_json(ctk_root, "12.9.20250531") + lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") + + monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) + + def fail_query_driver_cuda_version() -> DriverCudaVersion: + raise QueryDriverCudaVersionError("driver query failed") + + monkeypatch.setattr(compatibility_module, "query_driver_cuda_version", fail_query_driver_cuda_version) + + pfchecks = WithCompatibilityChecks() + + with pytest.raises( + CompatibilityCheckError, + match="Failed to query the CUDA driver version needed for compatibility checks", + ) as exc_info: + pfchecks.load_nvidia_dynamic_lib("nvrtc") + + assert isinstance(exc_info.value.__cause__, QueryDriverCudaVersionError) + + def test_find_nvidia_header_directory_returns_none_when_unresolved(monkeypatch): monkeypatch.setattr( compatibility_module, From a450104ec6e0ec3f903678b3ae8c5c19291199c5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 21 Apr 2026 15:19:23 -0700 Subject: [PATCH 16/18] Remove pathfinder driver info public re-exports Stop exposing the new driver info helper through cuda.pathfinder while keeping the internal implementation and internal test coverage in place. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/__init__.py | 3 --- cuda_pathfinder/docs/source/api.rst | 4 ---- cuda_pathfinder/tests/test_utils_driver_info.py | 15 --------------- 3 files changed, 22 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 89becb1834f..dc818dfd08f 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -59,9 +59,6 @@ from cuda.pathfinder._static_libs.find_static_lib import ( locate_static_lib as locate_static_lib, ) -from cuda.pathfinder._utils.driver_info import DriverCudaVersion as DriverCudaVersion -from cuda.pathfinder._utils.driver_info import QueryDriverCudaVersionError as QueryDriverCudaVersionError -from cuda.pathfinder._utils.driver_info import query_driver_cuda_version as query_driver_cuda_version from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home as get_cuda_path_or_home from cuda.pathfinder._version import __version__ # isort: skip diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 7fe9ea54e9d..e49478c09ec 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -18,10 +18,6 @@ CUDA bitcode and static libraries. get_cuda_path_or_home - DriverCudaVersion - QueryDriverCudaVersionError - query_driver_cuda_version - SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib LoadedDL diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 9ebb98e4fb6..21948dadafe 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -5,15 +5,6 @@ import pytest -from cuda.pathfinder import ( - DriverCudaVersion as PublicDriverCudaVersion, -) -from cuda.pathfinder import ( - QueryDriverCudaVersionError as PublicQueryDriverCudaVersionError, -) -from cuda.pathfinder import ( - query_driver_cuda_version as public_query_driver_cuda_version, -) from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL from cuda.pathfinder._utils import driver_info @@ -51,12 +42,6 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) -def test_driver_cuda_version_public_api_exports(): - assert PublicDriverCudaVersion is driver_info.DriverCudaVersion - assert PublicQueryDriverCudaVersionError is driver_info.QueryDriverCudaVersionError - assert public_query_driver_cuda_version is driver_info.query_driver_cuda_version - - def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] From f952144f945a4179af90c6f8d4c1bed341e4f66b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 21 Apr 2026 16:52:57 -0700 Subject: [PATCH 17/18] Privatize pathfinder driver info API in compatibility branch Align the compatibility-checks branch with the internal-only driver info helper by dropping the public re-exports and updating docs and tests to use the internal module directly. Made-with: Cursor --- cuda_pathfinder/cuda/pathfinder/__init__.py | 3 --- cuda_pathfinder/docs/source/api.rst | 4 ---- cuda_pathfinder/tests/test_utils_driver_info.py | 15 --------------- .../tests/test_with_compatibility_checks.py | 3 +-- 4 files changed, 1 insertion(+), 24 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 7e760f83b9c..3105523f8f5 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -68,9 +68,6 @@ from cuda.pathfinder._static_libs.find_static_lib import ( locate_static_lib as locate_static_lib, ) -from cuda.pathfinder._utils.driver_info import DriverCudaVersion as DriverCudaVersion -from cuda.pathfinder._utils.driver_info import QueryDriverCudaVersionError as QueryDriverCudaVersionError -from cuda.pathfinder._utils.driver_info import query_driver_cuda_version as query_driver_cuda_version from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home as get_cuda_path_or_home from cuda.pathfinder._version import __version__ # isort: skip diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 101c650bea5..1c58d4f41c9 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -22,10 +22,6 @@ CUDA bitcode and static libraries. CompatibilityCheckError CompatibilityInsufficientMetadataError - DriverCudaVersion - QueryDriverCudaVersionError - query_driver_cuda_version - SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib LoadedDL diff --git a/cuda_pathfinder/tests/test_utils_driver_info.py b/cuda_pathfinder/tests/test_utils_driver_info.py index 9ebb98e4fb6..21948dadafe 100644 --- a/cuda_pathfinder/tests/test_utils_driver_info.py +++ b/cuda_pathfinder/tests/test_utils_driver_info.py @@ -5,15 +5,6 @@ import pytest -from cuda.pathfinder import ( - DriverCudaVersion as PublicDriverCudaVersion, -) -from cuda.pathfinder import ( - QueryDriverCudaVersionError as PublicQueryDriverCudaVersionError, -) -from cuda.pathfinder import ( - query_driver_cuda_version as public_query_driver_cuda_version, -) from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL from cuda.pathfinder._utils import driver_info @@ -51,12 +42,6 @@ def _loaded_cuda(abs_path: str) -> LoadedDL: ) -def test_driver_cuda_version_public_api_exports(): - assert PublicDriverCudaVersion is driver_info.DriverCudaVersion - assert PublicQueryDriverCudaVersionError is driver_info.QueryDriverCudaVersionError - assert public_query_driver_cuda_version is driver_info.query_driver_cuda_version - - def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch): fake_driver_lib = _FakeDriverLib(status=0, version=12080) loaded_paths: list[str] = [] diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py index d51427c2e26..f228cee8230 100644 --- a/cuda_pathfinder/tests/test_with_compatibility_checks.py +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -12,16 +12,15 @@ BitcodeLibNotFoundError, CompatibilityCheckError, CompatibilityInsufficientMetadataError, - DriverCudaVersion, DynamicLibNotFoundError, LoadedDL, LocatedBitcodeLib, LocatedHeaderDir, LocatedStaticLib, - QueryDriverCudaVersionError, StaticLibNotFoundError, WithCompatibilityChecks, ) +from cuda.pathfinder._utils.driver_info import DriverCudaVersion, QueryDriverCudaVersionError STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_WITH_COMPATIBILITY_CHECKS_STRICTNESS", "see_what_works") assert STRICTNESS in ("see_what_works", "all_must_work") From 6a6ef09d34c7ebc8fd3d81c55c95e6c183ad5c76 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 24 Apr 2026 08:46:55 -0700 Subject: [PATCH 18/18] Use DriverCudaVersion throughout pathfinder compatibility checks. Keep the compatibility wrapper aligned with the shared driver-info API by carrying `DriverCudaVersion` through the checks, constructor, tests, and real-driver logging instead of falling back to encoded ints. Made-with: Cursor --- .../cuda/pathfinder/_compatibility_checks.py | 31 ++++++++--------- .../tests/test_driver_lib_loading.py | 14 +++++--- .../tests/test_with_compatibility_checks.py | 34 ++++++++++++------- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py b/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py index af3a4ed2131..b6bc2301470 100644 --- a/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py +++ b/cuda_pathfinder/cuda/pathfinder/_compatibility_checks.py @@ -42,6 +42,7 @@ locate_static_lib as _locate_static_lib, ) from cuda.pathfinder._utils.driver_info import ( + DriverCudaVersion, QueryDriverCudaVersionError, query_driver_cuda_version, ) @@ -50,7 +51,6 @@ PackagedWith: TypeAlias = str ConstraintOperator: TypeAlias = str ConstraintArg: TypeAlias = int | str | tuple[str, int] | None -DriverVersionArg: TypeAlias = int | None _CTK_VERSION_RE = re.compile(r"^(?P\d+)\.(?P\d+)") _REQUIRES_DIST_RE = re.compile( @@ -166,10 +166,6 @@ def _coerce_constraint(name: str, raw_value: ConstraintArg) -> ComparisonConstra raise ValueError(f"{name} must be an int, a (operator, value) tuple, or a string like '>=12'.") -def _driver_major(driver_version: int) -> int: - return driver_version // 1000 - - def _parse_ctk_version(cuda_version: str) -> CtkVersion | None: match = _CTK_VERSION_RE.match(cuda_version) if match is None: @@ -378,7 +374,9 @@ def _resolve_binary_item(utility_name: str, abs_path: str) -> ResolvedItem: ) -def compatibility_check(driver_version: int, item1: ResolvedItem, item2: ResolvedItem) -> CompatibilityResult: +def compatibility_check( + driver_cuda_version: DriverCudaVersion, item1: ResolvedItem, item2: ResolvedItem +) -> CompatibilityResult: for item in (item1, item2): if item.packaged_with != "ctk": return CompatibilityResult( @@ -411,12 +409,11 @@ def compatibility_check(driver_version: int, item1: ResolvedItem, item2: Resolve ), ) - driver_major = _driver_major(driver_version) - if driver_major < item1.ctk_version.major: + if driver_cuda_version.major < item1.ctk_version.major: return CompatibilityResult( status="incompatible", message=( - f"Driver version {driver_version} only supports CUDA major version {driver_major}, " + f"Driver version {driver_cuda_version.encoded} only supports CUDA major version {driver_cuda_version.major}, " f"but {item1.describe()} requires CTK {item1.ctk_version}. " "v1 requires driver_major >= ctk_major." ), @@ -426,7 +423,7 @@ def compatibility_check(driver_version: int, item1: ResolvedItem, item2: Resolve status="compatible", message=( f"{item1.describe()} and {item2.describe()} both resolve to CTK {item1.ctk_version}, " - f"and driver version {driver_version} satisfies the v1 driver guard rail." + f"and driver version {driver_cuda_version.encoded} satisfies the v1 driver guard rail." ), ) @@ -439,22 +436,22 @@ def __init__( *, ctk_major: ConstraintArg = None, ctk_minor: ConstraintArg = None, - driver_version: DriverVersionArg = None, + driver_cuda_version: DriverCudaVersion | None = None, ) -> None: self._ctk_major_constraint = _coerce_constraint("ctk_major", ctk_major) self._ctk_minor_constraint = _coerce_constraint("ctk_minor", ctk_minor) - self._driver_version = driver_version + self._driver_cuda_version = driver_cuda_version self._resolved_items: list[ResolvedItem] = [] - def _get_driver_version(self) -> int: - if self._driver_version is None: + def _get_driver_cuda_version(self) -> DriverCudaVersion: + if self._driver_cuda_version is None: try: - self._driver_version = query_driver_cuda_version().encoded + self._driver_cuda_version = query_driver_cuda_version() except QueryDriverCudaVersionError as exc: raise CompatibilityCheckError( "Failed to query the CUDA driver version needed for compatibility checks." ) from exc - return self._driver_version + return self._driver_cuda_version def _enforce_supported_packaging(self, item: ResolvedItem) -> None: if item.packaged_with == "ctk": @@ -502,7 +499,7 @@ def _register_and_check(self, item: ResolvedItem) -> None: anchor = self._anchor_item() if anchor is None: anchor = item - compatibility_check(self._get_driver_version(), anchor, item).require_compatible() + compatibility_check(self._get_driver_cuda_version(), anchor, item).require_compatible() self._remember(item) def load_nvidia_dynamic_lib(self, libname: str) -> LoadedDL: diff --git a/cuda_pathfinder/tests/test_driver_lib_loading.py b/cuda_pathfinder/tests/test_driver_lib_loading.py index b97453c9b5a..188640179c0 100644 --- a/cuda_pathfinder/tests/test_driver_lib_loading.py +++ b/cuda_pathfinder/tests/test_driver_lib_loading.py @@ -164,7 +164,7 @@ def test_real_query_driver_cuda_version(info_summary_append): driver_info._load_nvidia_dynamic_lib.cache_clear() driver_info.query_driver_cuda_version.cache_clear() try: - version = driver_info.query_driver_cuda_version() + driver_cuda_version = driver_info.query_driver_cuda_version() except driver_info.QueryDriverCudaVersionError as exc: if STRICTNESS == "all_must_work": raise @@ -174,7 +174,11 @@ def test_real_query_driver_cuda_version(info_summary_append): driver_info._load_nvidia_dynamic_lib.cache_clear() driver_info.query_driver_cuda_version.cache_clear() - info_summary_append(f"driver_version={version.major}.{version.minor} (encoded={version.encoded})") - assert version.encoded > 0 - assert version.major == version.encoded // 1000 - assert version.minor == (version.encoded % 1000) // 10 + info_summary_append( + "driver_cuda_version=" + f"{driver_cuda_version.major}.{driver_cuda_version.minor} " + f"(encoded={driver_cuda_version.encoded})" + ) + assert driver_cuda_version.encoded > 0 + assert driver_cuda_version.major == driver_cuda_version.encoded // 1000 + assert driver_cuda_version.minor == (driver_cuda_version.encoded % 1000) // 10 diff --git a/cuda_pathfinder/tests/test_with_compatibility_checks.py b/cuda_pathfinder/tests/test_with_compatibility_checks.py index f228cee8230..88017bb0fe4 100644 --- a/cuda_pathfinder/tests/test_with_compatibility_checks.py +++ b/cuda_pathfinder/tests/test_with_compatibility_checks.py @@ -65,6 +65,14 @@ def _located_bitcode_lib(name: str, abs_path: str) -> LocatedBitcodeLib: ) +def _driver_cuda_version(encoded: int) -> DriverCudaVersion: + return DriverCudaVersion( + encoded=encoded, + major=encoded // 1000, + minor=(encoded % 1000) // 10, + ) + + def _assert_real_ctk_backed_path(path: str) -> None: norm_path = os.path.normpath(os.path.abspath(path)) if "site-packages" in Path(norm_path).parts: @@ -103,7 +111,7 @@ def test_load_dynamic_lib_then_find_headers_same_ctk_version(monkeypatch, tmp_pa lambda _libname: LocatedHeaderDir(abs_path=str(hdr_dir), found_via="CUDA_PATH"), ) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") hdr_path = pfchecks.find_nvidia_header_directory("nvrtc") @@ -129,7 +137,7 @@ def test_exact_ctk_major_minor_match_is_required(monkeypatch, tmp_path): lambda _libname: LocatedHeaderDir(abs_path=str(hdr_dir), found_via="CUDA_PATH"), ) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) pfchecks.load_nvidia_dynamic_lib("nvrtc") with pytest.raises(CompatibilityCheckError, match="exact CTK major.minor match"): @@ -143,7 +151,7 @@ def test_driver_major_must_not_be_older_than_ctk_major(monkeypatch, tmp_path): monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) - pfchecks = WithCompatibilityChecks(driver_version=12080) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(12080)) with pytest.raises(CompatibilityCheckError, match="driver_major >= ctk_major"): pfchecks.load_nvidia_dynamic_lib("nvrtc") @@ -154,7 +162,7 @@ def test_missing_version_json_raises_insufficient_metadata(monkeypatch, tmp_path monkeypatch.setattr(compatibility_module, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_dl(lib_path)) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) with pytest.raises(CompatibilityInsufficientMetadataError, match="version.json"): pfchecks.load_nvidia_dynamic_lib("nvrtc") @@ -169,7 +177,7 @@ def test_other_packaging_raises_insufficient_metadata(monkeypatch, tmp_path): lambda _name: _located_bitcode_lib("nvshmem_device", abs_path), ) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) with pytest.raises(CompatibilityInsufficientMetadataError, match="packaged_with='ctk'"): pfchecks.find_bitcode_lib("nvshmem_device") @@ -185,7 +193,7 @@ def test_constraints_accept_string_and_tuple_forms(monkeypatch, tmp_path): pfchecks = WithCompatibilityChecks( ctk_major=(">=", 12), ctk_minor=">=9", - driver_version=13000, + driver_cuda_version=_driver_cuda_version(13000), ) loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") @@ -203,7 +211,7 @@ def test_constraint_failure_raises(monkeypatch, tmp_path): pfchecks = WithCompatibilityChecks( ctk_major=12, ctk_minor="<9", - driver_version=13000, + driver_cuda_version=_driver_cuda_version(13000), ) with pytest.raises(CompatibilityCheckError, match="ctk_minor<9"): @@ -236,7 +244,7 @@ def test_static_bitcode_and_binary_methods_participate_in_checks(monkeypatch, tm lambda _utility_name: binary_path, ) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) pfchecks.load_nvidia_dynamic_lib("nvrtc") assert pfchecks.find_static_lib("cudadevrt") == static_path @@ -244,7 +252,7 @@ def test_static_bitcode_and_binary_methods_participate_in_checks(monkeypatch, tm assert pfchecks.find_nvidia_binary_utility("nvcc") == binary_path -def test_wrapper_queries_driver_version_by_default(monkeypatch, tmp_path): +def test_wrapper_queries_driver_cuda_version_by_default(monkeypatch, tmp_path): ctk_root = tmp_path / "cuda-12.9" _write_version_json(ctk_root, "12.9.20250531") lib_path = _touch(ctk_root / "targets" / "x86_64-linux" / "lib" / "libnvrtc.so.12") @@ -255,7 +263,7 @@ def test_wrapper_queries_driver_version_by_default(monkeypatch, tmp_path): def fake_query_driver_cuda_version() -> DriverCudaVersion: query_calls.append(1) - return DriverCudaVersion(encoded=13000, major=13, minor=0) + return _driver_cuda_version(13000) monkeypatch.setattr(compatibility_module, "query_driver_cuda_version", fake_query_driver_cuda_version) @@ -297,13 +305,13 @@ def test_find_nvidia_header_directory_returns_none_when_unresolved(monkeypatch): lambda _libname: None, ) - pfchecks = WithCompatibilityChecks(driver_version=13000) + pfchecks = WithCompatibilityChecks(driver_cuda_version=_driver_cuda_version(13000)) assert pfchecks.find_nvidia_header_directory("nvrtc") is None def test_real_wheel_ctk_items_are_compatible(info_summary_append): - pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_version=13000) + pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_cuda_version=_driver_cuda_version(13000)) try: loaded = pfchecks.load_nvidia_dynamic_lib("nvrtc") @@ -337,7 +345,7 @@ def test_real_wheel_ctk_items_are_compatible(info_summary_append): def test_real_wheel_component_version_does_not_override_ctk_line(info_summary_append): - pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_version=13000) + pfchecks = WithCompatibilityChecks(ctk_major=13, ctk_minor=2, driver_cuda_version=_driver_cuda_version(13000)) try: header_dir = pfchecks.find_nvidia_header_directory("cufft")