Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cuda_bindings/cuda/bindings/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions cuda_bindings/cuda/bindings/utils/_envvar.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions cuda_bindings/cuda/bindings/utils/_version_check.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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,
)
51 changes: 51 additions & 0 deletions cuda_bindings/tests/test_envvar.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions cuda_bindings/tests/test_version_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading