From 7aa52a96beca2c5f6836d30a37104e2458bd8c38 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sun, 9 Aug 2026 13:31:23 -0700 Subject: [PATCH] fix(core): make cuda.core.system actually fall back when NVML is unimportable The file's own header states the contract: use NVML exclusively, or when `cuda.bindings.nvml` is not available fall back to non-NVML methods. The code does neither: if CUDA_BINDINGS_NVML_IS_COMPATIBLE: try: from cuda.bindings import nvml except ImportError: CUDA_BINDINGS_NVML_IS_COMPATIBLE = False from cuda.core.system._nvml_context import initialize else: from cuda.core._utils.cuda_utils import driver, handle_return, runtime The `except` clears the flag, but the `else` belongs to the outer `if`, which has already been evaluated -- so the fallback names are never bound on the path that clears the flag. Every consumer keys off the now-False flag and reaches for exactly those names: get_user_mode_driver_version() -> handle_return(driver.cuDriverGetVersion()) get_num_devices() -> handle_return(runtime.cudaGetDeviceCount()) both of which would raise `NameError`. In practice the module never gets that far: `_nvml_context` is imported unconditionally on the next line and `_nvml_context.pyx` starts with `from cuda.bindings import nvml`, so the ImportError the try/except exists to absorb is re-raised out of `import cuda.core.system` a statement later. Move the `_nvml_context` import inside the same `try` and turn the `else` into a second `if not ...` so a cleared flag selects the fallback. --- cuda_core/cuda/core/system/_system.pyx | 10 ++++- cuda_core/docs/source/release/1.2.0-notes.rst | 8 ++++ cuda_core/tests/system/test_system_system.py | 43 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cuda_core/cuda/core/system/_system.pyx b/cuda_core/cuda/core/system/_system.pyx index 2a6c8ffc23d..183f022aafb 100644 --- a/cuda_core/cuda/core/system/_system.pyx +++ b/cuda_core/cuda/core/system/_system.pyx @@ -47,11 +47,17 @@ else: if CUDA_BINDINGS_NVML_IS_COMPATIBLE: try: from cuda.bindings import nvml + # _nvml_context imports cuda.bindings.nvml itself, so it has to be + # inside the same try: importing it after nvml failed would raise the + # very ImportError this block exists to absorb. + from cuda.core.system._nvml_context import initialize except ImportError: CUDA_BINDINGS_NVML_IS_COMPATIBLE = False - from cuda.core.system._nvml_context import initialize -else: +# Deliberately a second `if`, not an `else` on the one above: the flag can be +# cleared by the import that just failed, and the non-NVML fallbacks are +# exactly what every consumer below reaches for once it is False. +if not CUDA_BINDINGS_NVML_IS_COMPATIBLE: from cuda.core._utils.cuda_utils import driver, handle_return, runtime diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index 120d2c2a253..d82ad7e2b2d 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -73,6 +73,14 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- ``cuda.core.system`` now really does fall back to its non-NVML + implementations when ``cuda.bindings.nvml`` cannot be imported. The + ``except ImportError`` cleared ``CUDA_BINDINGS_NVML_IS_COMPATIBLE``, but the + ``else`` that binds ``driver`` / ``handle_return`` / ``runtime`` hung off the + outer ``if``, which had already been evaluated, and the unconditional + ``_nvml_context`` import that followed pulls in ``cuda.bindings.nvml`` + itself -- so ``import cuda.core.system`` raised instead of degrading. + Deprecation Notices ------------------- diff --git a/cuda_core/tests/system/test_system_system.py b/cuda_core/tests/system/test_system_system.py index 28173836ff4..14eeefe3743 100644 --- a/cuda_core/tests/system/test_system_system.py +++ b/cuda_core/tests/system/test_system_system.py @@ -4,6 +4,8 @@ import os +import subprocess +import sys import pytest from cuda_python_test_helpers.arch_check import skip_if_nvml_unsupported @@ -80,3 +82,44 @@ def test_get_driver_branch(): driver_branch = system.get_driver_branch() assert isinstance(driver_branch, str) assert len(driver_branch) > 0 + + +# The NVML-unavailable fallback is decided at import time, so it can only be +# exercised in a fresh interpreter with cuda.bindings.nvml blocked. +_NO_NVML_SCRIPT = """ +import sys + + +class _BlockNvml: + def find_spec(self, name, path=None, target=None): + if name == "cuda.bindings.nvml": + raise ImportError("blocked for testing", name=name) + return None + + +sys.meta_path.insert(0, _BlockNvml()) + +# Used to raise ImportError out of this import: the flag was cleared, but the +# `else` that binds the non-NVML fallbacks belonged to the outer `if`, which +# had already been evaluated, and _nvml_context (imported unconditionally +# right after) imports cuda.bindings.nvml itself. +from cuda.core import system +from cuda.core.system import _system + +assert system.CUDA_BINDINGS_NVML_IS_COMPATIBLE is False, "flag must be cleared when nvml is unimportable" +for name in ("driver", "handle_return", "runtime"): + assert hasattr(_system, name), f"non-NVML fallback {name!r} is not bound" +print("ok") +""" + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_system_falls_back_when_nvml_is_unimportable(): + proc = subprocess.run( # noqa: S603 + [sys.executable, "-c", _NO_NVML_SCRIPT], + capture_output=True, + text=True, + timeout=300, + ) + assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" + assert proc.stdout.strip().endswith("ok")