From a8ebc34f6c300a90bc8a505e41fde2f8204aa9a9 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 8 Aug 2026 18:13:37 -0700 Subject: [PATCH] Make check_nvvm_compiler_options return False when nvvm is unavailable check_nvvm_compiler_options() has two guards meant to answer "options not supported" instead of failing when nvvm is missing. Neither one works. 1. The optional import. try: from cuda.bindings import nvvm except ModuleNotFoundError as exc: if exc.name == "nvvm": return False raise `from import ` never raises ModuleNotFoundError for a missing submodule: importlib swallows it in _handle_fromlist() and the IMPORT_FROM opcode raises a plain ImportError. And ModuleNotFoundError.name is always the fully qualified name, so even where one is raised it is "cuda.bindings.nvvm", never "nvvm". In a tree where the nvvm extension has not been built, the public API therefore raises ImportError: cannot import name 'nvvm' from 'cuda.bindings' Import via importlib.import_module(), which does raise ModuleNotFoundError with name == "cuda.bindings.nvvm", and compare against that. This mirrors cuda.pathfinder._optional_cuda_import, which already uses the fully qualified name for the same "is the target module itself missing, or one of its dependencies?" distinction. A missing dependency still propagates, unchanged. 2. The libNVVM probe. if _inspect_function_pointer("__nvvmCreateProgram") == 0: return False A zero pointer only covers "libNVVM is loaded but does not export the symbol". _inspect_function_pointer() loads libNVVM lazily, so when it is not installed at all the call raises DynamicLibNotFoundError instead of returning 0. tests/test_utils.py already knows this: its _is_libnvvm_available() helper wraps the identical call in `except DynamicLibNotFoundError`. Catch it here too. This is what test_check_nvvm_compiler_options_no_libnvvm asserts, and that test errors out on a machine without libNVVM today. It is on the always-skipped list in #2077, which is why nobody has seen it fail. Adds three tests that simulate both conditions without needing an unbuilt tree or a machine without libNVVM, so they run in CI. --- .../cuda/bindings/utils/_nvvm_utils.py | 22 ++++-- cuda_bindings/tests/test_utils.py | 73 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py index 9ac37c3a236..5af0d7d18de 100644 --- a/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py +++ b/cuda_bindings/cuda/bindings/utils/_nvvm_utils.py @@ -1,8 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +import importlib from typing import Sequence +_NVVM_MODULE_NAME = "cuda.bindings.nvvm" + _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" @@ -51,15 +54,24 @@ def check_nvvm_compiler_options(options: Sequence[str]) -> bool: True """ try: - from cuda.bindings import nvvm + nvvm = importlib.import_module(_NVVM_MODULE_NAME) except ModuleNotFoundError as exc: - if exc.name == "nvvm": - return False - raise + if exc.name != _NVVM_MODULE_NAME: + # A dependency of cuda.bindings.nvvm is missing, not the module + # itself. Never mask that: it is a real problem worth reporting. + raise + return False from cuda.bindings._internal.nvvm import _inspect_function_pointer + from cuda.pathfinder import DynamicLibNotFoundError - if _inspect_function_pointer("__nvvmCreateProgram") == 0: + try: + if _inspect_function_pointer("__nvvmCreateProgram") == 0: + return False + except DynamicLibNotFoundError: + # A zero function pointer means libNVVM is loaded but does not export + # the symbol. When libNVVM is not installed at all, the lazy loader + # behind _inspect_function_pointer raises instead. return False program = nvvm.create_program() diff --git a/cuda_bindings/tests/test_utils.py b/cuda_bindings/tests/test_utils.py index c767996bced..ccffc85a208 100644 --- a/cuda_bindings/tests/test_utils.py +++ b/cuda_bindings/tests/test_utils.py @@ -5,6 +5,7 @@ import random import subprocess import sys +import types from pathlib import Path import pytest @@ -169,3 +170,75 @@ 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 + + +class _RaiseOnImport: + """A sys.meta_path finder that makes one module fail to import.""" + + def __init__(self, fullname: str, exc: BaseException): + self._fullname = fullname + self._exc = exc + + def find_spec(self, fullname, path=None, target=None): + if fullname == self._fullname: + raise self._exc + return None + + +def _simulate_import_failure(monkeypatch, exc: BaseException) -> None: + """Make ``cuda.bindings.nvvm`` unimportable for the duration of a test.""" + import cuda.bindings + + # Both the parent-package attribute and the sys.modules entry short-circuit + # the import system, so a previously imported nvvm has to be hidden too. + monkeypatch.delattr(cuda.bindings, "nvvm", raising=False) + monkeypatch.delitem(sys.modules, "cuda.bindings.nvvm", raising=False) + monkeypatch.setattr(sys, "meta_path", [_RaiseOnImport("cuda.bindings.nvvm", exc), *sys.meta_path]) + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_check_nvvm_compiler_options_without_the_nvvm_binding(monkeypatch): + """An absent cuda.bindings.nvvm means "options unsupported", not a crash. + + This is reachable from a source checkout in which the nvvm extension has + not been built yet. + """ + _simulate_import_failure( + monkeypatch, + ModuleNotFoundError("No module named 'cuda.bindings.nvvm'", name="cuda.bindings.nvvm"), + ) + assert check_nvvm_compiler_options(["-arch=compute_90"]) is False + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_check_nvvm_compiler_options_does_not_mask_a_missing_dependency(monkeypatch): + """Only the nvvm module itself is optional; a broken dependency must surface.""" + _simulate_import_failure( + monkeypatch, + ModuleNotFoundError("No module named 'not_a_real_dependency'", name="not_a_real_dependency"), + ) + with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency"): + check_nvvm_compiler_options(["-arch=compute_90"]) + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_check_nvvm_compiler_options_without_libnvvm(monkeypatch): + """This is what test_check_nvvm_compiler_options_no_libnvvm above hits for real. + + That test only runs on a machine without libNVVM, so it never runs in CI + (see #2077). Simulate the same condition here: _inspect_function_pointer() + loads libNVVM lazily and raises DynamicLibNotFoundError when it is absent. + """ + import cuda.bindings._internal as internal_pkg + from cuda.pathfinder import DynamicLibNotFoundError + + def raise_not_found(_name): + raise DynamicLibNotFoundError("libnvvm not found (simulated)") + + fake = types.ModuleType("cuda.bindings._internal.nvvm") + fake._inspect_function_pointer = raise_not_found + # Cover both routes a `from ... import ...` can take to the submodule. + monkeypatch.setitem(sys.modules, "cuda.bindings._internal.nvvm", fake) + monkeypatch.setattr(internal_pkg, "nvvm", fake, raising=False) + + assert check_nvvm_compiler_options(["-arch=compute_90"]) is False