Skip to content
3 changes: 2 additions & 1 deletion cuda_bindings/cuda/bindings/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
Comment thread
abhilash1910 marked this conversation as resolved.
# 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

Expand Down
89 changes: 89 additions & 0 deletions cuda_bindings/cuda/bindings/utils/_nvvm_utils.py
Original file line number Diff line number Diff line change
@@ -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[bytes]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still Sequence[bytes] (should be 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_options
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I overlooked before: this should be check_nvvm_compiler_options (import and call).

>>> check_nvvm_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
52 changes: 51 additions & 1 deletion cuda_bindings/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,28 @@

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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except Exception here is overly broad and could mask bugs (all kinds of runtime errors).

Since cuda.bindings._internal.nvvm is internal to cuda_bindings, the import should be unconditional — if it fails, that's a bug we want to surface.

The only expected exception is DynamicLibNotFoundError (or subclasses) when libNVVM isn't installed.

Suggested change:

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

try:
from cuda.bindings._internal.nvvm import _inspect_function_pointer

return _inspect_function_pointer("__nvvmCreateProgram") != 0
except Exception:
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
Expand Down Expand Up @@ -118,3 +136,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
124 changes: 35 additions & 89 deletions cuda_core/tests/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,81 +70,26 @@ 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.
def _has_check_nvvm_compiler_options():
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be made slightly more explicit and tighter by separating the module import from the attribute check:

def _has_check_nvvm_compiler_options():
    try:
        import cuda.bindings.utils as utils
    except ModuleNotFoundError:
        return False
    return hasattr(utils, "check_nvvm_compiler_options")

This makes the intent clearer: first check if the module exists, then check if the function is available. The ModuleNotFoundError guard handles very old cuda-bindings, while hasattr handles bindings that have the module but predate this new function.

try:
from cuda.bindings.utils import check_nvvm_compiler_options # noqa: F401

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
return True
except ImportError:
return False

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")
Expand Down Expand Up @@ -524,10 +469,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(
Expand All @@ -539,17 +487,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",
),
],
),
],
)
Expand Down Expand Up @@ -729,12 +683,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
Expand All @@ -747,12 +697,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
Expand Down