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")