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 1/2] 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 dc818dfd08..e445134753 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 0000000000..ec185bec44 --- /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 e49478c09e..1c58d4f41c 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 0000000000..1b332fce59 --- /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 2/2] 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 1b332fce59..1b8ab1c6ba 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)