diff --git a/cuda_bindings/cuda/bindings/utils/__init__.py b/cuda_bindings/cuda/bindings/utils/__init__.py index 0bfff4b78be..f76b07ee2f9 100644 --- a/cuda_bindings/cuda/bindings/utils/__init__.py +++ b/cuda_bindings/cuda/bindings/utils/__init__.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any, Callable +from ._envvar import envvar_bool 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 diff --git a/cuda_bindings/cuda/bindings/utils/_envvar.py b/cuda_bindings/cuda/bindings/utils/_envvar.py new file mode 100644 index 00000000000..16a812f0035 --- /dev/null +++ b/cuda_bindings/cuda/bindings/utils/_envvar.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +_TRUE_VALUES = frozenset({"1", "true", "yes", "on"}) +_FALSE_VALUES = frozenset({"0", "false", "no", "off"}) + + +def envvar_bool(name: str, default: bool = False) -> bool: + """Read a bool-like environment variable. + + Unset, empty, or whitespace-only means ``default``. ``1/true/yes/on`` and + ``0/false/no/off`` are recognised case-insensitively, and any other integer + follows C truthiness, so ``2`` is true and ``-0`` is false. + + A value that is none of those keeps the historical set-means-true + behaviour rather than raising, because these variables are read during + import and a raise would turn a typo into an import failure. + """ + raw = os.environ.get(name) + if raw is None: + return default + raw = raw.strip() + if not raw: + return default + lowered = raw.lower() + if lowered in _TRUE_VALUES: + return True + if lowered in _FALSE_VALUES: + return False + try: + return int(raw, 0) != 0 + except ValueError: + return True diff --git a/cuda_bindings/cuda/bindings/utils/_version_check.py b/cuda_bindings/cuda/bindings/utils/_version_check.py index 5c68b50152e..d05c313b7a1 100644 --- a/cuda_bindings/cuda/bindings/utils/_version_check.py +++ b/cuda_bindings/cuda/bindings/utils/_version_check.py @@ -1,14 +1,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import os import threading import warnings +from ._envvar import envvar_bool + # Track whether we've already checked major version compatibility _major_version_compatibility_checked = False _lock = threading.Lock() +_DISABLE_WARNING_ENV_VAR = "CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING" + def warn_if_cuda_major_version_mismatch(): """Warn if the CUDA driver major version is older than cuda-bindings compile-time version. @@ -21,7 +24,8 @@ def warn_if_cuda_major_version_mismatch(): The check runs only once per process. Subsequent calls are no-ops. The warning can be suppressed by setting the environment variable - ``CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING=1``. + ``CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING=1``. Setting it to ``0`` (or + leaving it unset or empty) keeps the warning enabled. """ global _major_version_compatibility_checked if _major_version_compatibility_checked: @@ -32,7 +36,7 @@ def warn_if_cuda_major_version_mismatch(): _major_version_compatibility_checked = True # Allow users to suppress the warning - if os.environ.get("CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING"): + if envvar_bool(_DISABLE_WARNING_ENV_VAR): return # Import here to avoid circular imports and allow lazy loading @@ -55,7 +59,7 @@ def warn_if_cuda_major_version_mismatch(): f"NVIDIA driver only supports up to CUDA {runtime_major}. Some cuda-bindings " f"features may not work correctly. Consider updating your NVIDIA driver, " f"or using a cuda-bindings version built for CUDA {runtime_major}. " - f"(Set CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING=1 to suppress this warning.)", + f"(Set {_DISABLE_WARNING_ENV_VAR}=1 to suppress this warning.)", UserWarning, stacklevel=3, ) diff --git a/cuda_bindings/tests/test_envvar.py b/cuda_bindings/tests/test_envvar.py new file mode 100644 index 00000000000..a1f2d016910 --- /dev/null +++ b/cuda_bindings/tests/test_envvar.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from cuda.bindings.utils import envvar_bool + +_VAR = "CUDA_PYTHON_TEST_ENVVAR_BOOL" + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + ("raw", "expected"), + [ + pytest.param("0", False, id="zero"), + pytest.param(" 0 ", False, id="zero-padded"), + pytest.param("", False, id="empty"), + pytest.param(" ", False, id="blank"), + pytest.param("1", True, id="one"), + pytest.param("2", True, id="two"), + pytest.param("-0", False, id="negative-zero"), + pytest.param("false", False, id="false"), + pytest.param("FALSE", False, id="false-upper"), + pytest.param("no", False, id="no"), + pytest.param("off", False, id="off"), + pytest.param("true", True, id="true"), + pytest.param("True", True, id="true-capitalised"), + pytest.param("yes", True, id="yes"), + pytest.param("on", True, id="on"), + # Anything unrecognised keeps the historical set-means-true behaviour. + pytest.param("banana", True, id="unrecognised"), + ], +) +def test_envvar_bool_parsing(monkeypatch, raw, expected): + monkeypatch.setenv(_VAR, raw) + assert envvar_bool(_VAR) is expected + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize("default", [False, True]) +def test_envvar_bool_unset_returns_default(monkeypatch, default): + monkeypatch.delenv(_VAR, raising=False) + assert envvar_bool(_VAR, default) is default + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize("raw", ["", " "]) +def test_envvar_bool_blank_returns_default(monkeypatch, raw): + """Blank is "not set", so it must not override a True default.""" + monkeypatch.setenv(_VAR, raw) + assert envvar_bool(_VAR, True) is True diff --git a/cuda_bindings/tests/test_version_check.py b/cuda_bindings/tests/test_version_check.py index 03c3d7d3c2c..009322433d2 100644 --- a/cuda_bindings/tests/test_version_check.py +++ b/cuda_bindings/tests/test_version_check.py @@ -84,6 +84,27 @@ def test_warning_suppressed_by_env_var(self): warn_if_cuda_major_version_mismatch() assert len(w) == 0 + @pytest.mark.agent_authored(model="claude-opus-5") + def test_warning_not_suppressed_when_env_var_is_zero(self): + """``=0`` is how a user says "no, keep warning me". + + A bare truthiness test on the raw string made ``=0`` suppress the + warning -- the opposite of what the warning itself tells the user to + type, and the opposite of the other boolean knobs in this repository + (``CUDA_PYTHON_CUDA_PER_THREAD_DEFAULT_STREAM``, + ``CUDA_CORE_DONT_FIX_TAB_COMPLETION``), which both parse with ``int()``. + """ + with ( + mock.patch.object(driver, "CUDA_VERSION", 13000), + mock.patch.object(driver, "cuDriverGetVersion", return_value=(driver.CUresult.CUDA_SUCCESS, 12080)), + mock.patch.dict(os.environ, {"CUDA_PYTHON_DISABLE_MAJOR_VERSION_WARNING": "0"}), + warnings.catch_warnings(record=True) as w, + ): + warnings.simplefilter("always") + warn_if_cuda_major_version_mismatch() + assert len(w) == 1 + assert issubclass(w[0].category, UserWarning) + def test_error_when_driver_version_fails(self): """Should raise RuntimeError if cuDriverGetVersion fails.""" with (