diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index 4dbb9950de..3fd4255ba7 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -29,6 +29,7 @@ from dataclasses import dataclass from typing import Union from warnings import warn +from cuda.pathfinder import optional_cuda_import from cuda.core._device import Device from cuda.core._module import ObjectCode from cuda.core._utils.clear_error_support import assert_type @@ -649,23 +650,20 @@ def _decide_nvjitlink_or_driver() -> bool: " For best results, consider upgrading to a recent version of" ) - try: - __import__("cuda.bindings.nvjitlink") # availability check - except ModuleNotFoundError: + nvjitlink_module = optional_cuda_import( + "cuda.bindings.nvjitlink", + probe_function=lambda module: module.version(), # probe triggers nvJitLink runtime load + ) + if nvjitlink_module is None: warn_txt = f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." else: from cuda.bindings._internal import nvjitlink - try: - if _nvjitlink_has_version_symbol(nvjitlink): - _use_nvjitlink_backend = True - return False # Use nvjitlink - except RuntimeError: - warn_detail = "not available" - else: - warn_detail = "too old (<12.3)" + if _nvjitlink_has_version_symbol(nvjitlink): + _use_nvjitlink_backend = True + return False # Use nvjitlink warn_txt = ( - f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is {warn_detail}." + f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)." f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink." ) diff --git a/cuda_core/cuda/core/_program.pyx b/cuda_core/cuda/core/_program.pyx index 0b1fa93279..5e85a111e7 100644 --- a/cuda_core/cuda/core/_program.pyx +++ b/cuda_core/cuda/core/_program.pyx @@ -14,6 +14,7 @@ import threading from warnings import warn from cuda.bindings import driver, nvrtc +from cuda.pathfinder import optional_cuda_import from libcpp.vector cimport vector @@ -461,8 +462,8 @@ class ProgramOptions: # ============================================================================= # Module-level state for NVVM lazy loading -cdef object_nvvm_module = None -cdef bint _nvvm_import_attempted = False +_nvvm_module = None +_nvvm_import_attempted = False def _get_nvvm_module(): @@ -484,18 +485,21 @@ def _get_nvvm_module(): "Please update cuda-bindings to use NVVM features." ) - from cuda.bindings import nvvm - from cuda.bindings._internal.nvvm import _inspect_function_pointer - - if _inspect_function_pointer("__nvvmCreateProgram") == 0: - raise RuntimeError("NVVM library (libnvvm) is not available in this Python environment. ") + nvvm = optional_cuda_import( + "cuda.bindings.nvvm", + probe_function=lambda module: module.version(), # probe triggers libnvvm load + ) + if nvvm is None: + raise RuntimeError( + "NVVM support is unavailable: cuda.bindings.nvvm is missing or libnvvm cannot be loaded." + ) _nvvm_module = nvvm return _nvvm_module - except RuntimeError as e: + except RuntimeError: _nvvm_module = None - raise e + raise def _find_libdevice_path(): """Find libdevice*.bc for NVVM compilation using cuda.pathfinder.""" diff --git a/cuda_core/docs/source/release/0.7.x-notes.rst b/cuda_core/docs/source/release/0.7.x-notes.rst index 0fb0817581..7df4574a95 100644 --- a/cuda_core/docs/source/release/0.7.x-notes.rst +++ b/cuda_core/docs/source/release/0.7.x-notes.rst @@ -40,3 +40,6 @@ Fixes and enhancements linking operations to the C level and releasing the GIL during backend calls. This benefits workloads that create many programs or linkers, and enables concurrent compilation in multithreaded applications. +- Improved optional dependency handling for NVVM and nvJitLink imports so that only genuinely + missing optional modules are treated as unavailable; unrelated import failures now surface + normally, and ``cuda.core`` now depends directly on ``cuda-pathfinder``. diff --git a/cuda_core/pixi.lock b/cuda_core/pixi.lock index 2536544389..15736cbe8b 100644 --- a/cuda_core/pixi.lock +++ b/cuda_core/pixi.lock @@ -32,7 +32,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-12.9.86-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-12.9.86-h4bc722e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-tools-12.9.86-h4bc722e_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-profiler-api-12.9.79-h7938cbb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-12.9-h4f385c5_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cython-3.2.2-py314h1807b08_0.conda @@ -214,6 +213,7 @@ environments: build: py314h59f3c06_0 - conda: ../cuda_bindings build: py314h59f3c06_0 + - conda: ../cuda_pathfinder linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda @@ -240,7 +240,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-12.9.86-h579c4fd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-12.9.86-h7b14b0b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-tools-12.9.86-h7b14b0b_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-profiler-api-12.9.79-h16bee8c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-12.9-h4f385c5_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cython-3.2.2-py314h4c416a3_0.conda @@ -412,6 +411,7 @@ environments: build: py314ha479ada_0 - conda: ../cuda_bindings build: py314ha479ada_0 + - conda: ../cuda_pathfinder win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda @@ -435,7 +435,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-12.9.86-h57928b3_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-12.9.86-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-tools-12.9.86-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-profiler-api-12.9.79-h57928b3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-12.9-h4f385c5_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cython-3.2.2-py314h344ed54_0.conda @@ -556,6 +555,7 @@ environments: build: py314hae7e39d_0 - conda: ../cuda_bindings build: py314hae7e39d_0 + - conda: ../cuda_pathfinder cu13: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -588,7 +588,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.1.115-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.1.115-h4bc722e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-tools-13.1.115-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-profiler-api-13.1.80-h7938cbb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cython-3.2.2-py314h1807b08_0.conda @@ -770,6 +769,7 @@ environments: build: py314h59f3c06_0 - conda: ../cuda_bindings build: py314h59f3c06_0 + - conda: ../cuda_pathfinder linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda @@ -795,7 +795,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.1.115-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.1.115-h7b14b0b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-tools-13.1.115-h7b14b0b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-profiler-api-13.1.80-h16bee8c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cython-3.2.2-py314h4c416a3_0.conda @@ -967,6 +966,7 @@ environments: build: py314ha479ada_0 - conda: ../cuda_bindings build: py314ha479ada_0 + - conda: ../cuda_pathfinder win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda @@ -990,7 +990,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.1.115-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.1.115-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-tools-13.1.115-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-profiler-api-13.1.80-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cython-3.2.2-py314h344ed54_0.conda @@ -1111,6 +1110,7 @@ environments: build: py314hae7e39d_0 - conda: ../cuda_bindings build: py314hae7e39d_0 + - conda: ../cuda_pathfinder default: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -1143,7 +1143,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.1.115-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.1.115-h4bc722e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-tools-13.1.115-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-profiler-api-13.1.80-h7938cbb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cython-3.2.2-py314h1807b08_0.conda @@ -1325,6 +1324,7 @@ environments: build: py314h59f3c06_0 - conda: ../cuda_bindings build: py314h59f3c06_0 + - conda: ../cuda_pathfinder linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda @@ -1350,7 +1350,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.1.115-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.1.115-h7b14b0b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-tools-13.1.115-h7b14b0b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-profiler-api-13.1.80-h16bee8c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cython-3.2.2-py314h4c416a3_0.conda @@ -1522,6 +1521,7 @@ environments: build: py314ha479ada_0 - conda: ../cuda_bindings build: py314ha479ada_0 + - conda: ../cuda_pathfinder win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda @@ -1545,7 +1545,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.1.115-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.1.115-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-tools-13.1.115-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-profiler-api-13.1.80-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cython-3.2.2-py314h344ed54_0.conda @@ -1666,6 +1665,7 @@ environments: build: py314hae7e39d_0 - conda: ../cuda_bindings build: py314hae7e39d_0 + - conda: ../cuda_pathfinder examples: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -1699,7 +1699,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-64-13.1.115-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-impl-13.1.115-h4bc722e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-nvvm-tools-13.1.115-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.4.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cupy-14.0.1-py314h31ce861_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cupy-core-14.0.1-py314hed3c566_0.conda @@ -1901,6 +1900,7 @@ environments: build: py314h59f3c06_0 - conda: ../cuda_bindings build: py314h59f3c06_0 + - conda: ../cuda_pathfinder linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-7_kmp_llvm.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.15.3-he30d5cf_0.conda @@ -1929,7 +1929,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_linux-aarch64-13.1.115-h579c4fd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-impl-13.1.115-h7b14b0b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cuda-nvvm-tools-13.1.115-h7b14b0b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.4.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cupy-14.0.1-py314h8e5308c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cupy-core-14.0.1-py314h1d6db3a_0.conda @@ -2124,6 +2123,7 @@ environments: build: py314ha479ada_0 - conda: ../cuda_bindings build: py314ha479ada_0 + - conda: ../cuda_pathfinder win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda @@ -2135,7 +2135,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-nvvm-dev_win-64-13.1.115-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-impl-13.1.115-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cuda-nvvm-tools-13.1.115-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.4.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-version-13.1-h2ff5cdb_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-8.0.1-gpl_hb2d76f6_912.conda @@ -2223,6 +2222,7 @@ environments: build: py314hae7e39d_0 - conda: ../cuda_bindings build: py314hae7e39d_0 + - conda: ../cuda_pathfinder packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -2823,6 +2823,7 @@ packages: - python - numpy - cuda-bindings + - cuda-pathfinder - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -2833,6 +2834,8 @@ packages: sources: cuda-bindings: path: ../cuda_bindings + cuda-pathfinder: + path: ../cuda_pathfinder - conda: . name: cuda-core version: 0.5.0 @@ -2845,6 +2848,7 @@ packages: - python - numpy - cuda-bindings + - cuda-pathfinder - libgcc >=15 - libgcc >=15 - libstdcxx >=15 @@ -2855,6 +2859,8 @@ packages: sources: cuda-bindings: path: ../cuda_bindings + cuda-pathfinder: + path: ../cuda_pathfinder - conda: . name: cuda-core version: 0.5.0 @@ -2869,6 +2875,7 @@ packages: - python - numpy - cuda-bindings + - cuda-pathfinder - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 @@ -2878,6 +2885,8 @@ packages: sources: cuda-bindings: path: ../cuda_bindings + cuda-pathfinder: + path: ../cuda_pathfinder - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-64-12.9.86-ha770c72_2.conda sha256: e6257534c4b4b6b8a1192f84191c34906ab9968c92680fa09f639e7846a87304 md5: 79d280de61e18010df5997daea4743df @@ -3879,27 +3888,17 @@ packages: license: LicenseRef-NVIDIA-End-User-License-Agreement size: 41660022 timestamp: 1768280258661 -- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.3.3-pyhcf101f3_0.conda - sha256: 6f78993b194403725d4602355a8f1fc57f333eff9c3245a66f33e70c75d67163 - md5: b08fa4a3478526e33a4c08224398d2e5 - depends: - - python >=3.10 - - cuda-version >=12.0,<14 - - python - license: Apache-2.0 - size: 30869 - timestamp: 1764891530469 -- conda: https://conda.anaconda.org/conda-forge/noarch/cuda-pathfinder-1.4.0-pyhc364b38_0.conda - sha256: edf16fdfbcce5bbb445118fd8d070dda8afe36b4b437a94f472fde153bc38151 - md5: 2d13e524da66b60e6e7d5c6585729ea8 +- conda: ../cuda_pathfinder + name: cuda-pathfinder + version: 1.3.4a0 + build: pyh4616a5c_0 + subdir: noarch + variants: + target_platform: noarch depends: - python >=3.10 - - cuda-version >=12.0,<14 - - python + - python * license: Apache-2.0 - license_family: APACHE - size: 39327 - timestamp: 1772059437166 - conda: https://conda.anaconda.org/conda-forge/linux-64/cuda-profiler-api-12.9.79-h7938cbb_1.conda sha256: 4f679dfbf2bf2d17abb507f31b0176c0e3572337b5005b9e36179948a53988ac md5: 90d09865fb37d11d510444e34ebe6a09 diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index cd5b0d6de5..9dc6ac1ed9 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -148,6 +148,7 @@ numpy = "*" # Using path dependency now that we've added .pth support for Cython .pxd files # See build_hooks.py:_add_cython_include_paths_to_pth() cuda-bindings = { path = "../cuda_bindings" } +cuda-pathfinder = { path = "../cuda_pathfinder" } [target.linux.tasks.build-cython-tests] cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index a2828a9274..9b3e5a37c5 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -47,6 +47,7 @@ classifiers = [ "Environment :: GPU :: NVIDIA CUDA :: 13", ] dependencies = [ + "cuda-pathfinder >=1.4.2", "numpy", ] diff --git a/cuda_core/tests/test_optional_dependency_imports.py b/cuda_core/tests/test_optional_dependency_imports.py new file mode 100644 index 0000000000..25789f7f59 --- /dev/null +++ b/cuda_core/tests/test_optional_dependency_imports.py @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import types + +import pytest + +from cuda.core import _linker, _program + + +@pytest.fixture(autouse=True) +def restore_optional_import_state(): + saved_nvvm_module = _program._nvvm_module + saved_nvvm_attempted = _program._nvvm_import_attempted + saved_driver = _linker._driver + saved_driver_ver = _linker._driver_ver + saved_inited = _linker._inited + saved_use_nvjitlink = _linker._use_nvjitlink_backend + + _program._nvvm_module = None + _program._nvvm_import_attempted = False + _linker._driver = None + _linker._driver_ver = None + _linker._inited = False + _linker._use_nvjitlink_backend = False + + yield + + _program._nvvm_module = saved_nvvm_module + _program._nvvm_import_attempted = saved_nvvm_attempted + _linker._driver = saved_driver + _linker._driver_ver = saved_driver_ver + _linker._inited = saved_inited + _linker._use_nvjitlink_backend = saved_use_nvjitlink + + +def _patch_driver_version(monkeypatch, version=13000): + monkeypatch.setattr( + _linker, + "driver", + types.SimpleNamespace(cuDriverGetVersion=lambda: version), + ) + monkeypatch.setattr(_linker, "handle_return", lambda value: value) + + +def test_get_nvvm_module_reraises_nested_module_not_found(monkeypatch): + monkeypatch.setattr(_program, "get_binding_version", lambda: (12, 9)) + + def fake_optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvvm" + assert probe_function is not None + err = ModuleNotFoundError("No module named 'not_a_real_dependency'") + err.name = "not_a_real_dependency" + raise err + + monkeypatch.setattr(_program, "optional_cuda_import", fake_optional_cuda_import) + + with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency") as excinfo: + _program._get_nvvm_module() + assert excinfo.value.name == "not_a_real_dependency" + + +def test_get_nvvm_module_reports_missing_nvvm_module(monkeypatch): + monkeypatch.setattr(_program, "get_binding_version", lambda: (12, 9)) + + def fake_optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvvm" + assert probe_function is not None + return None + + monkeypatch.setattr(_program, "optional_cuda_import", fake_optional_cuda_import) + + with pytest.raises(RuntimeError, match="cuda.bindings.nvvm"): + _program._get_nvvm_module() + + +def test_get_nvvm_module_handles_missing_libnvvm(monkeypatch): + monkeypatch.setattr(_program, "get_binding_version", lambda: (12, 9)) + + def fake_optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvvm" + assert probe_function is not None + return None + + monkeypatch.setattr(_program, "optional_cuda_import", fake_optional_cuda_import) + + with pytest.raises(RuntimeError, match="libnvvm"): + _program._get_nvvm_module() + + +def test_decide_nvjitlink_or_driver_reraises_nested_module_not_found(monkeypatch): + _patch_driver_version(monkeypatch) + + def fake_optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is not None + err = ModuleNotFoundError("No module named 'not_a_real_dependency'") + err.name = "not_a_real_dependency" + raise err + + monkeypatch.setattr(_linker, "optional_cuda_import", fake_optional_cuda_import) + + with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency") as excinfo: + _linker._decide_nvjitlink_or_driver() + assert excinfo.value.name == "not_a_real_dependency" + + +def test_decide_nvjitlink_or_driver_falls_back_when_module_missing(monkeypatch): + _patch_driver_version(monkeypatch) + + def fake_optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is not None + return None + + monkeypatch.setattr(_linker, "optional_cuda_import", fake_optional_cuda_import) + + with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): + use_driver_backend = _linker._decide_nvjitlink_or_driver() + + assert use_driver_backend is True + assert _linker._use_nvjitlink_backend is False diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 57702de425..16711385b7 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -25,6 +25,7 @@ locate_nvidia_header_directory as locate_nvidia_header_directory, ) from cuda.pathfinder._headers.supported_nvidia_headers import SUPPORTED_HEADERS_CTK as _SUPPORTED_HEADERS_CTK +from cuda.pathfinder._optional_cuda_import import optional_cuda_import as optional_cuda_import from cuda.pathfinder._static_libs.find_bitcode_lib import ( SUPPORTED_BITCODE_LIBS as _SUPPORTED_BITCODE_LIBS, ) diff --git a/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py b/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py new file mode 100644 index 0000000000..3ac977cf35 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +from collections.abc import Callable +from types import ModuleType + +from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError + + +def optional_cuda_import( + fully_qualified_modname: str, + *, + probe_function: Callable[[ModuleType], object] | None = None, +) -> ModuleType | None: + """Import an optional CUDA module without masking unrelated import bugs. + + Returns: + The imported module if available and the optional probe succeeds, + otherwise ``None`` when the requested module is unavailable. + + Raises: + ModuleNotFoundError: If the import fails because a dependency of the + target module is missing (instead of the target module itself). + Exception: Any exception raised by ``probe_function`` except + :class:`DynamicLibNotFoundError`, which is treated as "unavailable". + """ + try: + module = importlib.import_module(fully_qualified_modname) + except ModuleNotFoundError as err: + if err.name != fully_qualified_modname: + raise + return None + + if probe_function is not None: + try: + probe_function(module) + except DynamicLibNotFoundError: + return None + + return module diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 52a4ff5010..63f4273a0a 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -14,6 +14,7 @@ locating NVIDIA C/C++ header directories, and finding CUDA binary utilities. SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib + optional_cuda_import LoadedDL DynamicLibNotFoundError DynamicLibUnknownError diff --git a/cuda_pathfinder/docs/source/release/1.4.2-notes.rst b/cuda_pathfinder/docs/source/release/1.4.2-notes.rst new file mode 100644 index 0000000000..f81ff2804f --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.4.2-notes.rst @@ -0,0 +1,15 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.4.2 Release notes +======================================= + +Highlights +---------- + +* Add ``optional_cuda_import()`` to support robust optional imports of CUDA + Python modules. It returns ``None`` when the requested module is absent or a + probe hits ``DynamicLibNotFoundError``, while still re-raising unrelated + ``ModuleNotFoundError`` exceptions (for missing transitive dependencies). diff --git a/cuda_pathfinder/tests/test_optional_cuda_import.py b/cuda_pathfinder/tests/test_optional_cuda_import.py new file mode 100644 index 0000000000..34cc2a158a --- /dev/null +++ b/cuda_pathfinder/tests/test_optional_cuda_import.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import types + +import pytest + +import cuda.pathfinder._optional_cuda_import as optional_import_mod +from cuda.pathfinder import DynamicLibNotFoundError, optional_cuda_import + + +def test_optional_cuda_import_returns_module_when_available(monkeypatch): + fake_module = types.SimpleNamespace(__name__="cuda.bindings.nvvm") + monkeypatch.setattr(optional_import_mod.importlib, "import_module", lambda _name: fake_module) + + result = optional_cuda_import("cuda.bindings.nvvm") + + assert result is fake_module + + +def test_optional_cuda_import_returns_none_when_module_missing(monkeypatch): + def fake_import_module(name): + err = ModuleNotFoundError("No module named 'cuda.bindings.nvvm'") + err.name = name + raise err + + monkeypatch.setattr(optional_import_mod.importlib, "import_module", fake_import_module) + + result = optional_cuda_import("cuda.bindings.nvvm") + + assert result is None + + +def test_optional_cuda_import_reraises_nested_module_not_found(monkeypatch): + def fake_import_module(_name): + err = ModuleNotFoundError("No module named 'not_a_real_dependency'") + err.name = "not_a_real_dependency" + raise err + + monkeypatch.setattr(optional_import_mod.importlib, "import_module", fake_import_module) + + with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency") as excinfo: + optional_cuda_import("cuda.bindings.nvvm") + assert excinfo.value.name == "not_a_real_dependency" + + +def test_optional_cuda_import_returns_none_when_probe_finds_missing_dynamic_lib(monkeypatch): + fake_module = types.SimpleNamespace(__name__="cuda.bindings.nvvm") + monkeypatch.setattr(optional_import_mod.importlib, "import_module", lambda _name: fake_module) + + def probe(_module): + raise DynamicLibNotFoundError("libnvvm missing") + + result = optional_cuda_import("cuda.bindings.nvvm", probe_function=probe) + + assert result is None + + +def test_optional_cuda_import_reraises_non_pathfinder_probe_error(monkeypatch): + fake_module = types.SimpleNamespace(__name__="cuda.bindings.nvvm") + monkeypatch.setattr(optional_import_mod.importlib, "import_module", lambda _name: fake_module) + + def probe(_module): + raise RuntimeError("unexpected probe failure") + + with pytest.raises(RuntimeError, match="unexpected probe failure"): + optional_cuda_import("cuda.bindings.nvvm", probe_function=probe)