Skip to content

Commit a8ebc34

Browse files
committed
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 <pkg> import <sub>` 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.
1 parent 3bd069a commit a8ebc34

2 files changed

Lines changed: 90 additions & 5 deletions

File tree

‎cuda_bindings/cuda/bindings/utils/_nvvm_utils.py‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import importlib
45
from typing import Sequence
56

7+
_NVVM_MODULE_NAME = "cuda.bindings.nvvm"
8+
69
_PRECHECK_NVVM_IR = """target triple = "nvptx64-unknown-cuda"
710
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"
811
@@ -51,15 +54,24 @@ def check_nvvm_compiler_options(options: Sequence[str]) -> bool:
5154
True
5255
"""
5356
try:
54-
from cuda.bindings import nvvm
57+
nvvm = importlib.import_module(_NVVM_MODULE_NAME)
5558
except ModuleNotFoundError as exc:
56-
if exc.name == "nvvm":
57-
return False
58-
raise
59+
if exc.name != _NVVM_MODULE_NAME:
60+
# A dependency of cuda.bindings.nvvm is missing, not the module
61+
# itself. Never mask that: it is a real problem worth reporting.
62+
raise
63+
return False
5964

6065
from cuda.bindings._internal.nvvm import _inspect_function_pointer
66+
from cuda.pathfinder import DynamicLibNotFoundError
6167

62-
if _inspect_function_pointer("__nvvmCreateProgram") == 0:
68+
try:
69+
if _inspect_function_pointer("__nvvmCreateProgram") == 0:
70+
return False
71+
except DynamicLibNotFoundError:
72+
# A zero function pointer means libNVVM is loaded but does not export
73+
# the symbol. When libNVVM is not installed at all, the lazy loader
74+
# behind _inspect_function_pointer raises instead.
6375
return False
6476

6577
program = nvvm.create_program()

‎cuda_bindings/tests/test_utils.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import random
66
import subprocess
77
import sys
8+
import types
89
from pathlib import Path
910

1011
import pytest
@@ -169,3 +170,75 @@ def test_check_nvvm_compiler_options_no_libnvvm():
169170
if _libnvvm_available:
170171
pytest.skip("libNVVM is available; this test targets the fallback path")
171172
assert check_nvvm_compiler_options(["-arch=compute_90"]) is False
173+
174+
175+
class _RaiseOnImport:
176+
"""A sys.meta_path finder that makes one module fail to import."""
177+
178+
def __init__(self, fullname: str, exc: BaseException):
179+
self._fullname = fullname
180+
self._exc = exc
181+
182+
def find_spec(self, fullname, path=None, target=None):
183+
if fullname == self._fullname:
184+
raise self._exc
185+
return None
186+
187+
188+
def _simulate_import_failure(monkeypatch, exc: BaseException) -> None:
189+
"""Make ``cuda.bindings.nvvm`` unimportable for the duration of a test."""
190+
import cuda.bindings
191+
192+
# Both the parent-package attribute and the sys.modules entry short-circuit
193+
# the import system, so a previously imported nvvm has to be hidden too.
194+
monkeypatch.delattr(cuda.bindings, "nvvm", raising=False)
195+
monkeypatch.delitem(sys.modules, "cuda.bindings.nvvm", raising=False)
196+
monkeypatch.setattr(sys, "meta_path", [_RaiseOnImport("cuda.bindings.nvvm", exc), *sys.meta_path])
197+
198+
199+
@pytest.mark.agent_authored(model="claude-opus-5")
200+
def test_check_nvvm_compiler_options_without_the_nvvm_binding(monkeypatch):
201+
"""An absent cuda.bindings.nvvm means "options unsupported", not a crash.
202+
203+
This is reachable from a source checkout in which the nvvm extension has
204+
not been built yet.
205+
"""
206+
_simulate_import_failure(
207+
monkeypatch,
208+
ModuleNotFoundError("No module named 'cuda.bindings.nvvm'", name="cuda.bindings.nvvm"),
209+
)
210+
assert check_nvvm_compiler_options(["-arch=compute_90"]) is False
211+
212+
213+
@pytest.mark.agent_authored(model="claude-opus-5")
214+
def test_check_nvvm_compiler_options_does_not_mask_a_missing_dependency(monkeypatch):
215+
"""Only the nvvm module itself is optional; a broken dependency must surface."""
216+
_simulate_import_failure(
217+
monkeypatch,
218+
ModuleNotFoundError("No module named 'not_a_real_dependency'", name="not_a_real_dependency"),
219+
)
220+
with pytest.raises(ModuleNotFoundError, match="not_a_real_dependency"):
221+
check_nvvm_compiler_options(["-arch=compute_90"])
222+
223+
224+
@pytest.mark.agent_authored(model="claude-opus-5")
225+
def test_check_nvvm_compiler_options_without_libnvvm(monkeypatch):
226+
"""This is what test_check_nvvm_compiler_options_no_libnvvm above hits for real.
227+
228+
That test only runs on a machine without libNVVM, so it never runs in CI
229+
(see #2077). Simulate the same condition here: _inspect_function_pointer()
230+
loads libNVVM lazily and raises DynamicLibNotFoundError when it is absent.
231+
"""
232+
import cuda.bindings._internal as internal_pkg
233+
from cuda.pathfinder import DynamicLibNotFoundError
234+
235+
def raise_not_found(_name):
236+
raise DynamicLibNotFoundError("libnvvm not found (simulated)")
237+
238+
fake = types.ModuleType("cuda.bindings._internal.nvvm")
239+
fake._inspect_function_pointer = raise_not_found
240+
# Cover both routes a `from ... import ...` can take to the submodule.
241+
monkeypatch.setitem(sys.modules, "cuda.bindings._internal.nvvm", fake)
242+
monkeypatch.setattr(internal_pkg, "nvvm", fake, raising=False)
243+
244+
assert check_nvvm_compiler_options(["-arch=compute_90"]) is False

0 commit comments

Comments
 (0)