forked from NVIDIA/cuda-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild_hooks.py
More file actions
282 lines (226 loc) · 11.3 KB
/
build_hooks.py
File metadata and controls
282 lines (226 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
# This module implements basic PEP 517 backend support, see e.g.
# - https://peps.python.org/pep-0517/
# - https://setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks
# Specifically, there are 5 APIs required to create a proper build backend, see below.
import functools
import glob
import os
import re
import sys
import tempfile
import zipfile
from pathlib import Path
from Cython.Build import cythonize
from setuptools import Extension
from setuptools import build_meta as _build_meta
prepare_metadata_for_build_editable = _build_meta.prepare_metadata_for_build_editable
prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel
build_sdist = _build_meta.build_sdist
get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist
COMPILE_FOR_COVERAGE = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0")))
@functools.cache
def _get_cuda_path() -> str:
# Not using cuda.pathfinder.get_cuda_path_or_home() here because this
# build backend runs in an isolated venv where the cuda namespace package
# from backend-path shadows the installed cuda-pathfinder. See #1803 for
# a workaround to apply after cuda-pathfinder >= 1.5 is released.
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME"))
if not cuda_path:
raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set")
print("CUDA path:", cuda_path)
return cuda_path
@functools.cache
def _determine_cuda_major_version() -> str:
"""Determine the CUDA major version for building cuda.core.
This version is used for two purposes:
1. Determining which cuda-bindings version to install as a build dependency
2. Setting CUDA_CORE_BUILD_MAJOR for Cython compile-time conditionals
The version is derived from (in order of priority):
1. CUDA_CORE_BUILD_MAJOR environment variable (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 (to provide include
directories), the cuda.h header should always be available.
"""
# Explicit override, e.g. in CI.
cuda_major = os.environ.get("CUDA_CORE_BUILD_MAJOR")
if cuda_major is not None:
print("CUDA MAJOR VERSION:", cuda_major)
return cuda_major
# Derive from the CUDA headers (the authoritative source for what we compile against).
cuda_path = _get_cuda_path()
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
try:
with open(cuda_h, encoding="utf-8") as f:
for line in f:
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
if m:
v = int(m.group(1))
# CUDA_VERSION is e.g. 12020 for 12.2.
cuda_major = str(v // 1000)
print("CUDA MAJOR VERSION:", cuda_major)
return cuda_major
except OSError:
pass
# CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here
# in normal circumstances. Raise an error to make the issue clear.
raise RuntimeError(
"Cannot determine CUDA major version. "
"Set CUDA_CORE_BUILD_MAJOR environment variable, or ensure CUDA_PATH or CUDA_HOME "
"points to a valid CUDA installation with include/cuda.h."
)
# used later by setup()
_extensions = None
def _build_cuda_core():
# Customizing the build hooks is needed because we must defer cythonization until cuda-bindings,
# now a required build-time dependency that's dynamically installed via the other hook below,
# is installed. Otherwise, cimport any cuda.bindings modules would fail!
#
# This function populates "_extensions".
global _extensions
# Add cuda-bindings to sys.path so Cython can find .pxd files
# This is needed for editable installs where meta path finders don't work for Cython
# We need to add the directory containing the 'cuda' package so Cython can resolve
# "from cuda.bindings cimport cydriver"
try:
import cuda.bindings
bindings_path = Path(cuda.bindings.__file__).parent # .../cuda/bindings/
cuda_package_dir = bindings_path.parent.parent # .../cuda_bindings/ (contains cuda/)
if str(cuda_package_dir) not in sys.path:
sys.path.insert(0, str(cuda_package_dir))
print(f"Added cuda-bindings parent path for Cython: {cuda_package_dir}", file=sys.stderr)
except ImportError:
# cuda-bindings not available in editable mode, will use installed version
pass
# It seems setuptools' wildcard support has problems for namespace packages,
# so we explicitly spell out all Extension instances.
def module_names():
root_path = os.path.sep.join(["cuda", "core", ""])
for filename in glob.glob(f"{root_path}/**/*.pyx", recursive=True):
yield filename[len(root_path) : -4]
def get_sources(mod_name):
"""Get source files for a module, including any .cpp files."""
sources = [f"cuda/core/{mod_name}.pyx"]
# Add module-specific .cpp file from _cpp/ directory if it exists
# Example: _resource_handles.pyx finds _cpp/resource_handles.cpp.
cpp_file = f"cuda/core/_cpp/{mod_name.lstrip('_')}.cpp"
if os.path.exists(cpp_file):
sources.append(cpp_file)
return sources
all_include_dirs = [os.path.join(_get_cuda_path(), "include")]
extra_compile_args = []
if COMPILE_FOR_COVERAGE:
# CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not
# related to free-threading builds.
extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1", "-DCYTHON_USE_SYS_MONITORING=0"]
ext_modules = tuple(
Extension(
f"cuda.core.{mod.replace(os.path.sep, '.')}",
sources=get_sources(mod),
include_dirs=[
"cuda/core/_include",
"cuda/core/_cpp",
]
+ all_include_dirs,
language="c++",
extra_compile_args=extra_compile_args,
)
for mod in module_names()
)
nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2))
compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(_determine_cuda_major_version())}
compiler_directives = {"embedsignature": True, "warn.deprecated.IF": False, "freethreading_compatible": True}
if COMPILE_FOR_COVERAGE:
compiler_directives["linetrace"] = True
_extensions = cythonize(
ext_modules,
verbose=True,
language_level=3,
build_dir="." if COMPILE_FOR_COVERAGE else "build/cython",
nthreads=nthreads,
compiler_directives=compiler_directives,
compile_time_env=compile_time_env,
)
return
def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
"""
Modify the .pth file in an editable install wheel to add Cython include paths.
This is needed because Cython cannot find .pxd files through meta path finders,
it only looks in sys.path directories. By adding direct paths to the .pth file,
we enable Cython to find .pxd files from editable-installed cuda-bindings.
See: https://github.com/scikit-build/scikit-build-core/pull/516
See: https://github.com/cython/cython/issues/7326
"""
# Find cuda-bindings location
# When building with pixi path dependencies, cuda-bindings should be importable
try:
import cuda.bindings
bindings_path = Path(cuda.bindings.__file__).parent # .../cuda/bindings/
# We need the directory containing the 'cuda' package for Cython imports
cuda_package_dir = bindings_path.parent.parent # .../cuda_bindings/ (contains cuda/)
print(f"Found cuda-bindings at: {bindings_path}", file=sys.stderr)
print(f"Will add to .pth for Cython: {cuda_package_dir}", file=sys.stderr)
except ImportError:
# If cuda-bindings isn't available yet, we can't add the path
# This might happen in some build scenarios, but it's okay - the
# wildcard dependency will work in those cases
print("cuda-bindings not found in current environment, skipping .pth modification")
return
# Create a temporary directory for wheel manipulation
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
wheel_file = Path(wheel_path)
# Extract the wheel
extract_dir = tmpdir_path / "extracted"
with zipfile.ZipFile(wheel_file, "r") as zf:
zf.extractall(extract_dir)
# Find the .pth file (should be named something like __editable___cuda_core-*.pth)
pth_files = list(extract_dir.glob("**/*.pth"))
if not pth_files:
print("Warning: No .pth file found in editable wheel", file=sys.stderr)
return
# Modify each .pth file (usually just one)
for pth_file in pth_files:
print(f"Modifying {pth_file.name} to add Cython include paths", file=sys.stderr)
# Read existing content
content = pth_file.read_text()
# Add the cuda-bindings source path to sys.path for Cython
# This allows Cython to find .pxd files via direct path lookup
# The path must be the directory containing the 'cuda' package
path_to_add = str(cuda_package_dir.absolute())
# Ensure content ends with newline before adding path
if not content.endswith("\n"):
content += "\n"
# Append to the .pth file (after the import hook line)
if path_to_add not in content:
pth_file.write_text(content + path_to_add + "\n")
print(f"Added Cython include path: {cuda_package_dir}", file=sys.stderr)
# Repackage the wheel
# Remove the old wheel first
wheel_file.unlink()
# Create new wheel with same name
with zipfile.ZipFile(wheel_file, "w", zipfile.ZIP_DEFLATED) as zf:
for file_path in extract_dir.rglob("*"):
if file_path.is_file():
arcname = file_path.relative_to(extract_dir)
zf.write(file_path, arcname)
print(f"Successfully patched {wheel_file.name}", file=sys.stderr)
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
_build_cuda_core()
wheel_name = _build_meta.build_editable(wheel_directory, config_settings, metadata_directory)
# Patch the .pth file to add Cython include paths
wheel_path = os.path.join(wheel_directory, wheel_name)
_add_cython_include_paths_to_pth(wheel_path)
return wheel_name
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
_build_cuda_core()
return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory)
def _get_cuda_bindings_require():
cuda_major = _determine_cuda_major_version()
return [f"cuda-bindings=={cuda_major}.*"]
def get_requires_for_build_editable(config_settings=None):
return _build_meta.get_requires_for_build_editable(config_settings) + _get_cuda_bindings_require()
def get_requires_for_build_wheel(config_settings=None):
return _build_meta.get_requires_for_build_wheel(config_settings) + _get_cuda_bindings_require()