diff --git a/cuda_bindings/cuda/bindings/utils/__init__.py b/cuda_bindings/cuda/bindings/utils/__init__.py index e4fcb15e8fd..3fe75ca13f0 100644 --- a/cuda_bindings/cuda/bindings/utils/__init__.py +++ b/cuda_bindings/cuda/bindings/utils/__init__.py @@ -1,7 +1,8 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE from typing import Any, Callable +from ._nvvm_utils import check_nvvm_compiler_options from ._ptx_utils import get_minimal_required_cuda_ver_from_ptx_ver, get_ptx_ver from ._version_check import warn_if_cuda_major_version_mismatch diff --git a/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py new file mode 100644 index 00000000000..de7b111e7bf --- /dev/null +++ b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +from typing import Sequence + +_PRECHECK_NVVM_IR = """target triple = "nvptx64-unknown-cuda" +target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-i128:128:128-f32:32:32-f64:64:64-v16:16:16-v32:32:32-v64:64:64-v128:128:128-n16:32:64" + +define void @dummy_kernel() {{ +entry: + ret void +}} + +!nvvm.annotations = !{{!0}} +!0 = !{{void ()* @dummy_kernel, !"kernel", i32 1}} + +!nvvmir.version = !{{!1}} +!1 = !{{i32 {major}, i32 {minor}, i32 {debug_major}, i32 {debug_minor}}} +""" + + +def check_nvvm_compiler_options(options: Sequence[str]) -> bool: + """ + Abstracted from https://github.com/NVIDIA/numba-cuda/pull/681 + + Check if the specified options are supported by the current libNVVM version. + + The options are a list of strings, each representing a compiler option. + + If the test program fails to compile, the options are not supported and False + is returned. + + If the test program compiles successfully, True is returned. + + cuda.bindings.nvvm returns exceptions instead of return codes. + + Parameters + ---------- + options : Sequence[str] + List of compiler options as strings (e.g., ["-arch=compute_90", "-g"]). + + Returns + ------- + bool + True if the options are supported, False otherwise. + + Examples + -------- + >>> from cuda.bindings.utils import check_nvvm_compiler_options + >>> check_nvvm_compiler_options(["-arch=compute_90", "-g"]) + True + """ + try: + from cuda.bindings import nvvm + except ModuleNotFoundError as exc: + if exc.name == "nvvm": + return False + raise + + from cuda.bindings._internal.nvvm import _inspect_function_pointer + + if _inspect_function_pointer("__nvvmCreateProgram") == 0: + return False + + program = nvvm.create_program() + try: + major, minor, debug_major, debug_minor = nvvm.ir_version() + precheck_ir = _PRECHECK_NVVM_IR.format( + major=major, + minor=minor, + debug_major=debug_major, + debug_minor=debug_minor, + ) + precheck_ir_bytes = precheck_ir.encode("utf-8") + nvvm.add_module_to_program( + program, + precheck_ir_bytes, + len(precheck_ir_bytes), + "precheck.ll", + ) + try: + nvvm.compile_program(program, len(options), options) + except nvvm.nvvmError as e: + if e.status == nvvm.Result.ERROR_INVALID_OPTION: + return False + raise + finally: + nvvm.destroy_program(program) + return True diff --git a/cuda_bindings/tests/test_utils.py b/cuda_bindings/tests/test_utils.py index fd89cd00bde..01e30269291 100644 --- a/cuda_bindings/tests/test_utils.py +++ b/cuda_bindings/tests/test_utils.py @@ -11,10 +11,29 @@ from cuda.bindings import driver, runtime from cuda.bindings._internal.utils import get_c_compiler -from cuda.bindings.utils import get_cuda_native_handle, get_minimal_required_cuda_ver_from_ptx_ver, get_ptx_ver +from cuda.bindings.utils import ( + check_nvvm_compiler_options, + get_cuda_native_handle, + get_minimal_required_cuda_ver_from_ptx_ver, + get_ptx_ver, +) have_cufile = importlib.util.find_spec("cuda.bindings.cufile") is not None + +def _is_libnvvm_available() -> bool: + from cuda.bindings._internal.nvvm import _inspect_function_pointer + from cuda.pathfinder import DynamicLibNotFoundError + + try: + return _inspect_function_pointer("__nvvmCreateProgram") != 0 + except DynamicLibNotFoundError: + return False + + +_libnvvm_available = _is_libnvvm_available() +_skip_no_libnvvm = pytest.mark.skipif(not _libnvvm_available, reason="libNVVM not available") + ptx_88_kernel = r""" .version 8.8 .target sm_75 @@ -118,3 +137,35 @@ def test_get_c_compiler(): c_compiler = get_c_compiler() prefix = ("GCC", "Clang", "MSVC", "Unknown") assert sum(c_compiler.startswith(p) for p in prefix) == 1 + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_valid(): + assert check_nvvm_compiler_options(["-arch=compute_90"]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_invalid(): + assert check_nvvm_compiler_options(["--this-is-not-a-valid-option"]) is False + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_empty(): + assert check_nvvm_compiler_options([]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_multiple_valid(): + assert check_nvvm_compiler_options(["-arch=compute_90", "-opt=3", "-g"]) is True + + +@_skip_no_libnvvm +def test_check_nvvm_compiler_options_arch_detection(): + assert check_nvvm_compiler_options(["-arch=compute_90"]) is True + assert check_nvvm_compiler_options(["-arch=compute_99999"]) is False + + +def test_check_nvvm_compiler_options_no_libnvvm(): + if _libnvvm_available: + pytest.skip("libNVVM is available; this test targets the fallback path") + assert check_nvvm_compiler_options(["-arch=compute_90"]) is False diff --git a/cuda_core/tests/test_program.py b/cuda_core/tests/test_program.py index 52107073a6c..992ce336555 100644 --- a/cuda_core/tests/test_program.py +++ b/cuda_core/tests/test_program.py @@ -70,81 +70,25 @@ def _has_nvrtc_pch_apis_for_tests(): ) -_libnvvm_version = None -_libnvvm_version_attempted = False - -precheck_nvvm_ir = """target triple = "nvptx64-unknown-cuda" -target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-i128:128:128-f32:32:32-f64:64:64-v16:16:16-v32:32:32-v64:64:64-v128:128:128-n16:32:64" - -define void @dummy_kernel() {{ - entry: - ret void -}} - -!nvvm.annotations = !{{!0}} -!0 = !{{void ()* @dummy_kernel, !"kernel", i32 1}} - -!nvvmir.version = !{{!1}} -!1 = !{{i32 {major}, i32 {minor}, i32 {debug_major}, i32 {debug_minor}}} -""" - - -def _get_libnvvm_version_for_tests(): - """ - Detect libNVVM version by compiling dummy IR and analyzing the PTX output. - - Workaround for the lack of direct libNVVM version API (nvbugs 5312315). - The approach: - - Compile a small dummy NVVM IR to PTX - - Use PTX version analysis APIs if available to infer libNVVM version - - Cache the result for future use - """ - global _libnvvm_version, _libnvvm_version_attempted +def _has_check_nvvm_compiler_options(): + try: + import cuda.bindings.utils as utils + except ModuleNotFoundError: + return False + return hasattr(utils, "check_nvvm_compiler_options") - if _libnvvm_version_attempted: - return _libnvvm_version - _libnvvm_version_attempted = True +has_nvvm_option_checker = pytest.mark.skipif( + not _has_check_nvvm_compiler_options(), + reason="cuda.bindings.utils.check_nvvm_compiler_options not available (cuda-bindings too old?)", +) - try: - from cuda.core._program import _get_nvvm_module - nvvm = _get_nvvm_module() - - try: - from cuda.bindings.utils import get_minimal_required_cuda_ver_from_ptx_ver, get_ptx_ver - except ImportError: - _libnvvm_version = None - return _libnvvm_version - - program = nvvm.create_program() - try: - major, minor, debug_major, debug_minor = nvvm.ir_version() - global precheck_nvvm_ir - precheck_nvvm_ir = precheck_nvvm_ir.format( - major=major, minor=minor, debug_major=debug_major, debug_minor=debug_minor - ) - precheck_ir_bytes = precheck_nvvm_ir.encode("utf-8") - nvvm.add_module_to_program(program, precheck_ir_bytes, len(precheck_ir_bytes), "precheck.ll") - - options = ["-arch=compute_90"] - nvvm.verify_program(program, len(options), options) - nvvm.compile_program(program, len(options), options) - - ptx_size = nvvm.get_compiled_result_size(program) - ptx_data = bytearray(ptx_size) - nvvm.get_compiled_result(program, ptx_data) - ptx_str = ptx_data.decode("utf-8") - ptx_version = get_ptx_ver(ptx_str) - cuda_version = get_minimal_required_cuda_ver_from_ptx_ver(ptx_version) - _libnvvm_version = cuda_version - return _libnvvm_version - finally: - nvvm.destroy_program(program) +def _check_nvvm_arch(arch: str) -> bool: + """Check if the given NVVM arch is supported by the installed libNVVM.""" + from cuda.bindings.utils import check_nvvm_compiler_options - except Exception: - _libnvvm_version = None - return _libnvvm_version + return check_nvvm_compiler_options([f"-arch={arch}"]) @pytest.fixture(scope="session") @@ -524,10 +468,13 @@ def test_nvvm_compile_invalid_ir(): ), pytest.param( ProgramOptions(name="test_sm110_1", arch="sm_110", device_code_optimize=False), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), pytest.param( ProgramOptions( @@ -539,17 +486,23 @@ def test_nvvm_compile_invalid_ir(): fma=True, device_code_optimize=True, ), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), pytest.param( ProgramOptions(name="test_sm110_3", arch="sm_110", link_time_optimization=True), - marks=pytest.mark.skipif( - (_get_libnvvm_version_for_tests() or 0) < 13000, - reason="Compute capability 110 requires libNVVM >= 13.0", - ), + marks=[ + has_nvvm_option_checker, + pytest.mark.skipif( + _has_check_nvvm_compiler_options() and not _check_nvvm_arch("compute_110"), + reason="Compute capability 110 not supported by installed libNVVM", + ), + ], ), ], ) @@ -729,12 +682,8 @@ def test_program_options_as_bytes_nvrtc(): """Test ProgramOptions.as_bytes() for NVRTC backend""" options = ProgramOptions(arch="sm_80", debug=True, lineinfo=True, ftz=True) nvrtc_options = options.as_bytes("nvrtc") - - # Should return list of bytes assert isinstance(nvrtc_options, list) assert all(isinstance(opt, bytes) for opt in nvrtc_options) - - # Decode to check content options_str = [opt.decode() for opt in nvrtc_options] assert "-arch=sm_80" in options_str assert "--device-debug" in options_str @@ -747,12 +696,8 @@ def test_program_options_as_bytes_nvvm(): """Test ProgramOptions.as_bytes() for NVVM backend""" options = ProgramOptions(arch="sm_80", debug=True, ftz=True, device_code_optimize=True) nvvm_options = options.as_bytes("nvvm") - - # Should return list of bytes (same as other backends) assert isinstance(nvvm_options, list) assert all(isinstance(opt, bytes) for opt in nvvm_options) - - # Decode to check content options_str = [opt.decode() for opt in nvvm_options] assert "-arch=compute_80" in options_str assert "-g" in options_str