Skip to content

Commit 1fb09d4

Browse files
committed
fix: derive CUDA major version from headers for build
Fixes build failures when cuda-bindings reports a different major version than the CUDA headers being compiled against. The new _get_cuda_major_version() function is used for both: 1. Determining which cuda-bindings version to install as a build dependency 2. Setting CUDA_CORE_BUILD_MAJOR for Cython compile-time conditionals Version is derived from (in order of priority): 1. CUDA_CORE_BUILD_MAJOR env var (explicit override, e.g. in CI) 2. CUDA_VERSION macro in cuda.h from CUDA_PATH or CUDA_HOME Since CUDA_PATH or CUDA_HOME is required for the build anyway, the cuda.h header should always be available, ensuring consistency between the installed cuda-bindings and the compile-time conditionals.
1 parent 51b9c6c commit 1fb09d4

2 files changed

Lines changed: 166 additions & 23 deletions

File tree

cuda_core/build_hooks.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import glob
1212
import os
1313
import re
14-
import subprocess
1514

1615
from Cython.Build import cythonize
1716
from setuptools import Extension
@@ -26,32 +25,48 @@
2625

2726

2827
@functools.cache
29-
def _get_proper_cuda_bindings_major_version() -> str:
30-
# for local development (with/without build isolation)
31-
try:
32-
import cuda.bindings
28+
def _get_cuda_major_version() -> str:
29+
"""Determine the CUDA major version for building cuda.core.
3330
34-
return cuda.bindings.__version__.split(".")[0]
35-
except ImportError:
36-
pass
31+
This version is used for two purposes:
32+
1. Determining which cuda-bindings version to install as a build dependency
33+
2. Setting CUDA_CORE_BUILD_MAJOR for Cython compile-time conditionals
3734
38-
# for custom overwrite, e.g. in CI
35+
The version is derived from (in order of priority):
36+
1. CUDA_CORE_BUILD_MAJOR environment variable (explicit override, e.g. in CI)
37+
2. CUDA_VERSION macro in cuda.h from CUDA_PATH or CUDA_HOME
38+
39+
Since CUDA_PATH or CUDA_HOME is required for the build (to provide include
40+
directories), the cuda.h header should always be available.
41+
"""
42+
# Explicit override, e.g. in CI.
3943
cuda_major = os.environ.get("CUDA_CORE_BUILD_MAJOR")
4044
if cuda_major is not None:
4145
return cuda_major
4246

43-
# also for local development
44-
try:
45-
out = subprocess.run("nvidia-smi", env=os.environ, capture_output=True, check=True) # noqa: S603, S607
46-
m = re.search(r"CUDA Version:\s*([\d\.]+)", out.stdout.decode())
47-
if m:
48-
return m.group(1).split(".")[0]
49-
except (FileNotFoundError, subprocess.CalledProcessError):
50-
# the build machine has no driver installed
51-
pass
52-
53-
# default fallback
54-
return "13"
47+
# Derive from the CUDA headers (the authoritative source for what we compile against).
48+
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None))
49+
if cuda_path:
50+
for root in cuda_path.split(os.pathsep):
51+
cuda_h = os.path.join(root, "include", "cuda.h")
52+
try:
53+
with open(cuda_h, encoding="utf-8") as f:
54+
for line in f:
55+
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
56+
if m:
57+
v = int(m.group(1))
58+
# CUDA_VERSION is e.g. 12020 for 12.2.
59+
return str(v // 1000)
60+
except OSError:
61+
continue
62+
63+
# CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here
64+
# in normal circumstances. Raise an error to make the issue clear.
65+
raise RuntimeError(
66+
"Cannot determine CUDA major version. "
67+
"Set CUDA_CORE_BUILD_MAJOR environment variable, or ensure CUDA_PATH or CUDA_HOME "
68+
"points to a valid CUDA installation with include/cuda.h."
69+
)
5570

5671

5772
# used later by setup()
@@ -105,7 +120,7 @@ def get_cuda_paths():
105120
)
106121

107122
nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2))
108-
compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(_get_proper_cuda_bindings_major_version())}
123+
compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(_get_cuda_major_version())}
109124
compiler_directives = {"embedsignature": True, "warn.deprecated.IF": False, "freethreading_compatible": True}
110125
if COMPILE_FOR_COVERAGE:
111126
compiler_directives["linetrace"] = True
@@ -132,7 +147,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
132147

133148

134149
def _get_cuda_bindings_require():
135-
cuda_major = _get_proper_cuda_bindings_major_version()
150+
cuda_major = _get_cuda_major_version()
136151
return [f"cuda-bindings=={cuda_major}.*"]
137152

138153

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Tests for build_hooks.py build infrastructure.
5+
6+
These tests verify the CUDA version detection logic used during builds,
7+
particularly the _get_cuda_major_version() function which derives the
8+
CUDA major version from headers.
9+
10+
Note: These tests do NOT require cuda.core to be built/installed since they
11+
test build-time infrastructure. Run with --noconftest to avoid loading
12+
conftest.py which imports cuda.core modules:
13+
14+
pytest tests/test_build_hooks.py -v --noconftest
15+
16+
These tests require Cython to be installed (build_hooks.py imports it).
17+
"""
18+
19+
import importlib.util
20+
import os
21+
import tempfile
22+
from pathlib import Path
23+
from unittest import mock
24+
25+
import pytest
26+
27+
# build_hooks.py imports Cython at the top level, so skip if not available
28+
pytest.importorskip("Cython")
29+
30+
31+
def _load_build_hooks():
32+
"""Load build_hooks module from source without permanently modifying sys.path.
33+
34+
build_hooks.py is a PEP 517 build backend, not an installed module.
35+
We use importlib to load it directly from source to avoid polluting
36+
sys.path with the cuda_core/ directory (which contains cuda/core/ source
37+
that could shadow the installed package).
38+
"""
39+
build_hooks_path = Path(__file__).parent.parent / "build_hooks.py"
40+
spec = importlib.util.spec_from_file_location("build_hooks", build_hooks_path)
41+
module = importlib.util.module_from_spec(spec)
42+
spec.loader.exec_module(module)
43+
return module
44+
45+
46+
# Load the module once at import time
47+
build_hooks = _load_build_hooks()
48+
49+
50+
def _check_version_detection(
51+
cuda_version, expected_major, *, use_cuda_path=True, use_cuda_home=False, cuda_core_build_major=None
52+
):
53+
"""Test version detection with a mock cuda.h.
54+
55+
Args:
56+
cuda_version: CUDA_VERSION to write in mock cuda.h (e.g., 12080)
57+
expected_major: Expected return value (e.g., "12")
58+
use_cuda_path: If True, set CUDA_PATH to the mock headers directory
59+
use_cuda_home: If True, set CUDA_HOME to the mock headers directory
60+
cuda_core_build_major: If set, override with this CUDA_CORE_BUILD_MAJOR env var
61+
"""
62+
with tempfile.TemporaryDirectory() as tmpdir:
63+
include_dir = Path(tmpdir) / "include"
64+
include_dir.mkdir()
65+
cuda_h = include_dir / "cuda.h"
66+
cuda_h.write_text(f"#define CUDA_VERSION {cuda_version}\n")
67+
68+
build_hooks._get_cuda_major_version.cache_clear()
69+
70+
mock_env = {
71+
k: v
72+
for k, v in {
73+
"CUDA_CORE_BUILD_MAJOR": cuda_core_build_major,
74+
"CUDA_PATH": tmpdir if use_cuda_path else None,
75+
"CUDA_HOME": tmpdir if use_cuda_home else None,
76+
}.items()
77+
if v is not None
78+
}
79+
80+
with mock.patch.dict(os.environ, mock_env, clear=True):
81+
result = build_hooks._get_cuda_major_version()
82+
assert result == expected_major
83+
84+
85+
class TestGetCudaMajorVersion:
86+
"""Tests for _get_cuda_major_version()."""
87+
88+
@pytest.mark.parametrize("version", ["11", "12", "13", "14"])
89+
def test_env_var_override(self, version):
90+
"""CUDA_CORE_BUILD_MAJOR env var override works with various versions."""
91+
build_hooks._get_cuda_major_version.cache_clear()
92+
with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False):
93+
result = build_hooks._get_cuda_major_version()
94+
assert result == version
95+
96+
@pytest.mark.parametrize(
97+
("cuda_version", "expected_major"),
98+
[
99+
(11000, "11"), # CUDA 11.0
100+
(11080, "11"), # CUDA 11.8
101+
(12000, "12"), # CUDA 12.0
102+
(12020, "12"), # CUDA 12.2
103+
(12080, "12"), # CUDA 12.8
104+
(13000, "13"), # CUDA 13.0
105+
(13010, "13"), # CUDA 13.1
106+
],
107+
ids=["11.0", "11.8", "12.0", "12.2", "12.8", "13.0", "13.1"],
108+
)
109+
def test_cuda_headers_parsing(self, cuda_version, expected_major):
110+
"""CUDA_VERSION is correctly parsed from cuda.h headers."""
111+
_check_version_detection(cuda_version, expected_major)
112+
113+
def test_cuda_home_fallback(self):
114+
"""CUDA_HOME is used if CUDA_PATH is not set."""
115+
_check_version_detection(12050, "12", use_cuda_path=False, use_cuda_home=True)
116+
117+
def test_env_var_takes_priority_over_headers(self):
118+
"""Env var override takes priority even when headers exist."""
119+
_check_version_detection(12080, "11", cuda_core_build_major="11")
120+
121+
def test_missing_cuda_path_raises_error(self):
122+
"""RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override."""
123+
build_hooks._get_cuda_major_version.cache_clear()
124+
with (
125+
mock.patch.dict(os.environ, {}, clear=True),
126+
pytest.raises(RuntimeError, match="Cannot determine CUDA major version"),
127+
):
128+
build_hooks._get_cuda_major_version()

0 commit comments

Comments
 (0)