From 8c03e6127188d59d34cda28adefb5a03384aa097 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 30 Sep 2026 14:19:15 -0700 Subject: [PATCH 1/3] ci: build Cython test extensions inside cibuildwheel Reuse the wheel Cython cache in tests/cython/build_tests.py so the new test stage can hit CUDA_PYTHON_CYTHON_CACHE_DIR. (cherry picked from commit 392178690f26c7d7cb8117a5556a70f2c728c321) --- cuda_bindings/tests/cython/build_tests.py | 63 ++++++++++++++++++--- cuda_core/tests/cython/build_tests.py | 69 +++++++++++++++++++---- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/cuda_bindings/tests/cython/build_tests.py b/cuda_bindings/tests/cython/build_tests.py index 5bde350e87b..1ea736f6749 100644 --- a/cuda_bindings/tests/cython/build_tests.py +++ b/cuda_bindings/tests/cython/build_tests.py @@ -8,19 +8,39 @@ so `cimport cuda.bindings.*` fails to locate the .pxd files. We resolve the namespace package's source root from `cuda.bindings.__file__` and pass it via `include_path=` so cythonize finds the .pxd tree on every platform. + +When CUDA_PYTHON_CYTHON_CACHE_DIR is set, cythonize uses the same cache +namespacing and include-path aliasing as cuda_bindings/build_hooks.py. """ from __future__ import annotations +import importlib.util import os import sys from pathlib import Path +import Cython from Cython.Build import cythonize from setuptools import setup import cuda.bindings +_COMPILER_DIRECTIVES = {"freethreading_compatible": True} + + +def _load_build_hooks(): + # PEP 517 backend, not an installed module. Load by path so we do not put + # cuda_bindings/ on sys.path (that would shadow the installed package). + build_hooks_path = Path(__file__).resolve().parents[2] / "build_hooks.py" + spec = importlib.util.spec_from_file_location("cuda_bindings_build_hooks", build_hooks_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +build_hooks = _load_build_hooks() + def _bindings_source_root() -> Path: # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py @@ -32,6 +52,41 @@ def _bindings_source_root() -> Path: return root +def _cythonize_tests(pyx_files): + cache_path = build_hooks._cython_cache_path( + "cuda-bindings-cython-tests", + compiler_directives=_COMPILER_DIRECTIVES, + language_level=3, + cplus=True, + ) + cythonize_kwargs = { + "language_level": 3, + "nthreads": 1, + "compiler_directives": _COMPILER_DIRECTIVES, + "cache": cache_path, + } + if cache_path is None: + return cythonize( + pyx_files, + include_path=[str(_bindings_source_root())], + **cythonize_kwargs, + ) + + # Distinct alias names so a concurrent package build's .cython-stdlib / + # .cython-bindings symlinks are not replaced. Relative aliases resolve + # next to build_hooks.py (package root). + stdlib_target = Path(Cython.__file__).parent / "Includes" + with build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib: + with build_hooks._stable_cython_alias( + _bindings_source_root(), Path(".cython-bindings-tests") + ) as rel_bindings: + return cythonize( + pyx_files, + include_path=[".", rel_bindings, rel_stdlib], + **cythonize_kwargs, + ) + + def main() -> None: script_dir = Path(__file__).resolve().parent # Avoid appending the absolute checkout path under build/temp: the @@ -41,13 +96,7 @@ def main() -> None: if not pyx_files: raise SystemExit(f"no test_*.pyx files under {script_dir}") - ext_modules = cythonize( - pyx_files, - language_level=3, - nthreads=1, - include_path=[str(_bindings_source_root())], - compiler_directives={"freethreading_compatible": True}, - ) + ext_modules = _cythonize_tests(pyx_files) # pytest imports each extension by bare module name (see test_cython.py), # so build in-place next to its .pyx regardless of the invoking cwd. diff --git a/cuda_core/tests/cython/build_tests.py b/cuda_core/tests/cython/build_tests.py index 6cebb0c6ff9..6518e9a679b 100644 --- a/cuda_core/tests/cython/build_tests.py +++ b/cuda_core/tests/cython/build_tests.py @@ -8,19 +8,39 @@ so `cimport cuda.bindings.*` fails to locate the .pxd files. We resolve the namespace package's source root from `cuda.bindings.__file__` and pass it via `include_path=` so cythonize finds the .pxd tree on every platform. + +When CUDA_PYTHON_CYTHON_CACHE_DIR is set, cythonize uses the same cache +namespacing and include-path aliasing as cuda_core/build_hooks.py. """ from __future__ import annotations +import importlib.util import os import sys from pathlib import Path +import Cython from Cython.Build import cythonize from setuptools import setup import cuda.bindings +_COMPILER_DIRECTIVES = {"freethreading_compatible": True} + + +def _load_build_hooks(): + # PEP 517 backend, not an installed module. Load by path so we do not put + # cuda_core/ on sys.path (that would shadow the installed package). + build_hooks_path = Path(__file__).resolve().parents[2] / "build_hooks.py" + spec = importlib.util.spec_from_file_location("cuda_core_build_hooks", build_hooks_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +build_hooks = _load_build_hooks() + def _bindings_source_root() -> Path: # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py @@ -32,26 +52,55 @@ def _bindings_source_root() -> Path: return root +def _cythonize_tests(pyx_files): + cache_path = build_hooks._cython_cache_path( + "cuda-core-cython-tests", + compiler_directives=_COMPILER_DIRECTIVES, + language_level=3, + cplus=True, + ) + cythonize_kwargs = { + "language_level": 3, + "compiler_directives": _COMPILER_DIRECTIVES, + "cache": cache_path, + } + if cache_path is None: + return cythonize( + pyx_files, + include_path=[str(_bindings_source_root())], + **cythonize_kwargs, + ) + + # Distinct alias names so a concurrent package build's .cython-stdlib / + # .cython-bindings symlinks are not replaced. Relative aliases resolve + # next to build_hooks.py (package root). + stdlib_target = Path(Cython.__file__).parent / "Includes" + with build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib: + with build_hooks._stable_cython_alias( + _bindings_source_root(), Path(".cython-bindings-tests") + ) as rel_bindings: + return cythonize( + pyx_files, + include_path=[".", rel_bindings, rel_stdlib], + **cythonize_kwargs, + ) + + def main() -> None: script_dir = Path(__file__).resolve().parent - pyx_files = sorted(str(p) for p in script_dir.glob("test_*.pyx")) + pyx_files = sorted(p.name for p in script_dir.glob("test_*.pyx")) if not pyx_files: raise SystemExit(f"no test_*.pyx files under {script_dir}") - ext_modules = cythonize( - pyx_files, - language_level=3, - include_path=[str(_bindings_source_root())], - compiler_directives={"freethreading_compatible": True}, - ) - # `build_ext --inplace` places the compiled .so relative to the current # working directory, but pixi runs this task from the project root. pytest # imports each extension by bare module name (see test_cython.py), which # only resolves when the .so sits in tests/cython (the dir pytest puts on - # sys.path). chdir here so the .so lands next to its .pyx regardless of the - # invoking cwd. + # sys.path). chdir before cythonize so alias relpaths and the .so location + # are stable regardless of the invoking cwd. os.chdir(script_dir) + ext_modules = _cythonize_tests(pyx_files) + sys.argv = [sys.argv[0], "build_ext", "--inplace"] setup(name="cuda_core_cython_tests", ext_modules=ext_modules) From 49be3fc9bbbca673e0536dce8c72c31005a58c6f Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 2 Oct 2026 07:56:30 -0700 Subject: [PATCH 2/3] build: unify compiler flag sets across cuda-bindings and cuda-core Close the audit from #1882. The two packages' Linux and MSVC flag sets had drifted for legacy reasons; both now go through the same structure. Changes in cuda-bindings: - Drop -fpermissive and -fno-var-tracking-assignments (gcc-only; not needed by current Cython-generated C++; confirmed by a rebuild). - Change -O3 to -O2 (consistent with cuda-core; -O3 was not the source of the measured launch_{256,512}_args latency difference vs c++17). - Add /std:c++14 and /O2 on MSVC (was missing entirely). - Split '-std=c++14 -Wno-deprecated-declarations' onto separate lines with an explanatory comment on each. Changes in cuda-core: - Add /O2 on MSVC (modern setuptools no longer forces /Ox). - Add comments at both the MSVC and Linux flag sites explaining why c++17 is required (structured bindings / if constexpr in _cpp/). Tests: - Update the bindings TestResolveToolchain: replace test_gnu_keeps_gcc_only_flags with test_linux_opt_flag_set (asserts -O2, not -O3; no gnu-only flags); add test_msvc_opt_flag_set; update test_gnu_sets_env_and_flags assertions. - Add test_linux_opt_flag_set and test_msvc_opt_flag_set to the core TestResolveToolchain; update the comment in test_gnu_sets_env_and_flags. --- cuda_bindings/build_hooks.py | 17 ++++++++---- cuda_bindings/tests/test_build_hooks.py | 36 ++++++++++++++++++------- cuda_core/build_hooks.py | 4 ++- cuda_core/tests/test_build_hooks.py | 28 +++++++++++++++++-- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index a1f572d7a37..eab8f7c6299 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -437,21 +437,28 @@ def _resolve_toolchain(debug=False, compile_for_coverage=False): extra_link_args = [] if name == "msvc": + # c++14: raising to c++17 costs a measured ~15% on launch_{256,512}_args + # from gcc's c++17 variadic-template expansion. + extra_compile_args += ["/std:c++14", "/O2"] if debug: raise RuntimeError("Debuggable builds are not supported on Windows.") else: # Common Linux compile flags. - extra_compile_args += ["-std=c++14", "-Wno-deprecated-declarations"] + # c++14: raising to c++17 costs a measured ~15% on launch_{256,512}_args + # from gcc's c++17 variadic-template expansion. + extra_compile_args += ["-std=c++14"] + # cudaMemcpy*Array* and cudaGetDriverEntryPoint are deprecated but still + # supported; suppress the resulting warnings so a future -Werror build + # is not broken by Cython-generated calls we cannot control. + extra_compile_args += ["-Wno-deprecated-declarations"] # Compiler-specific flags. - if name == "gnu": - extra_compile_args += ["-fpermissive", "-fno-var-tracking-assignments"] - elif name == "llvm": + if name == "llvm": extra_link_args += ["-fuse-ld=lld"] # Common Linux debug/opt flags. if debug: extra_compile_args += ["-g", "-O0", "-D _GLIBCXX_ASSERTIONS"] else: - extra_compile_args += ["-g0", "-O3"] + extra_compile_args += ["-g0", "-O2"] extra_link_args += ["-Wl,--strip-all"] if compile_for_coverage: diff --git a/cuda_bindings/tests/test_build_hooks.py b/cuda_bindings/tests/test_build_hooks.py index 78c41ed1885..b7606f997ba 100644 --- a/cuda_bindings/tests/test_build_hooks.py +++ b/cuda_bindings/tests/test_build_hooks.py @@ -148,9 +148,8 @@ def test_gnu_sets_env_and_flags(self, monkeypatch): assert os.environ["CXX"] == "g++" assert os.environ["LDCXXSHARED"] == "g++ -shared -Wl,-O1" assert "LDSHARED" not in os.environ - # gcc-only flags are present (this is the point of P2: explicit gnu must use gcc, not generic cc) - assert "-fpermissive" in cargs - assert "-fno-var-tracking-assignments" in cargs + assert "-fpermissive" not in cargs + assert "-fno-var-tracking-assignments" not in cargs @pytest.mark.agent_authored(model="grok-4.6") def test_llvm_keeps_sccache_prefix(self, monkeypatch): @@ -167,14 +166,31 @@ def test_llvm_keeps_sccache_prefix(self, monkeypatch): # The launcher prefixes CC/CXX only; the shared linker command is the bare compiler. assert os.environ["LDCXXSHARED"] == "clang++ -shared -Wl,-O1" - @pytest.mark.agent_authored(model="glm-5.2") - def test_gnu_keeps_gcc_only_flags(self, monkeypatch): + @pytest.mark.agent_authored(model="claude-sonnet-4-6") + def test_linux_opt_flag_set(self, monkeypatch): + """Linux opt build: -std=c++14, -O2, -Wno-deprecated-declarations; no -O3 or gnu-only flags.""" if sys.platform == "win32": - pytest.skip("gnu only valid on Linux") - monkeypatch.setenv("CUDA_PYTHON_TOOLCHAIN", "gnu") - _name, _cc, _cxx, cargs, _largs = build_hooks._resolve_toolchain() - assert "-fpermissive" in cargs - assert "-fno-var-tracking-assignments" in cargs + pytest.skip("Linux flags only") + monkeypatch.delenv("CUDA_PYTHON_TOOLCHAIN", raising=False) + _name, _cc, _cxx, cargs, largs = build_hooks._resolve_toolchain(debug=False) + assert "-std=c++14" in cargs + assert "-Wno-deprecated-declarations" in cargs + assert "-g0" in cargs + assert "-O2" in cargs + assert "-O3" not in cargs + assert "-fpermissive" not in cargs + assert "-fno-var-tracking-assignments" not in cargs + assert "-Wl,--strip-all" in largs + + @pytest.mark.agent_authored(model="claude-sonnet-4-6") + def test_msvc_opt_flag_set(self, monkeypatch): + """MSVC opt build emits /std:c++14 and /O2.""" + if sys.platform != "win32": + pytest.skip("MSVC flags only on Windows") + monkeypatch.delenv("CUDA_PYTHON_TOOLCHAIN", raising=False) + _name, _cc, _cxx, cargs, _largs = build_hooks._resolve_toolchain(debug=False) + assert "/std:c++14" in cargs + assert "/O2" in cargs @pytest.mark.agent_authored(model="claude-sonnet-5.5") def test_explicit_toolchain_prefers_env_ldcxxshared(self, monkeypatch): diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index afa956fa4d1..cf09a1bd288 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -484,11 +484,13 @@ def _resolve_toolchain(debug=False, compile_for_coverage=False): extra_link_args = [] if name == "msvc": - extra_compile_args += ["/std:c++17"] + # c++17: required by structured bindings and if constexpr in cuda/core/_cpp/. + extra_compile_args += ["/std:c++17", "/O2"] if debug: raise RuntimeError("Debuggable builds are not supported on Windows.") else: # Common Linux compile flags. + # c++17: required by structured bindings and if constexpr in cuda/core/_cpp/. extra_compile_args += ["-std=c++17"] # Compiler-specific flags. if name == "llvm": diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 7e9499f89e3..f5a431c9810 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -976,8 +976,8 @@ def test_gnu_sets_env_and_flags(self, monkeypatch): assert os.environ["CXX"] == "g++" assert os.environ["LDCXXSHARED"] == "g++ -shared -Wl,-O1" assert "LDSHARED" not in os.environ - # gcc-only flags are present (this is the point of P2: explicit gnu must use gcc, not generic cc) - assert "-fpermissive" not in cargs # cuda.core gnu flags don't include it; bindings do + # Neither cuda.core nor cuda.bindings sets these gcc-only flags. + assert "-fpermissive" not in cargs assert "-fno-var-tracking-assignments" not in cargs @pytest.mark.agent_authored(model="grok-4.6") @@ -1018,6 +1018,30 @@ def test_explicit_toolchain_falls_back_to_ldshared_then_shared_flag(self, monkey build_hooks._resolve_toolchain() assert os.environ["LDCXXSHARED"] == "clang++ -shared" + @pytest.mark.agent_authored(model="claude-sonnet-4-6") + def test_linux_opt_flag_set(self, monkeypatch): + """Linux opt build: -std=c++17, -O2; no gnu-only flags.""" + if sys.platform == "win32": + pytest.skip("Linux flags only") + monkeypatch.delenv("CUDA_PYTHON_TOOLCHAIN", raising=False) + _name, _cc, _cxx, cargs, largs = build_hooks._resolve_toolchain(debug=False) + assert "-std=c++17" in cargs + assert "-g0" in cargs + assert "-O2" in cargs + assert "-fpermissive" not in cargs + assert "-fno-var-tracking-assignments" not in cargs + assert "-Wl,--strip-all" in largs + + @pytest.mark.agent_authored(model="claude-sonnet-4-6") + def test_msvc_opt_flag_set(self, monkeypatch): + """MSVC opt build emits /std:c++17 and /O2.""" + if sys.platform != "win32": + pytest.skip("MSVC flags only on Windows") + monkeypatch.delenv("CUDA_PYTHON_TOOLCHAIN", raising=False) + _name, _cc, _cxx, cargs, _largs = build_hooks._resolve_toolchain(debug=False) + assert "/std:c++17" in cargs + assert "/O2" in cargs + class TestWithSccache: """_with_sccache: keep a leading sccache token, swap the compiler.""" From a04079f93acb2851e66981652c401ab7944a39ce Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 2 Oct 2026 08:05:29 -0700 Subject: [PATCH 3/3] fix: combine nested with statements in cython build_tests.py (SIM117) --- cuda_bindings/tests/cython/build_tests.py | 18 +++++++++--------- cuda_core/tests/cython/build_tests.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cuda_bindings/tests/cython/build_tests.py b/cuda_bindings/tests/cython/build_tests.py index 1ea736f6749..7fc609b1ec4 100644 --- a/cuda_bindings/tests/cython/build_tests.py +++ b/cuda_bindings/tests/cython/build_tests.py @@ -76,15 +76,15 @@ def _cythonize_tests(pyx_files): # .cython-bindings symlinks are not replaced. Relative aliases resolve # next to build_hooks.py (package root). stdlib_target = Path(Cython.__file__).parent / "Includes" - with build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib: - with build_hooks._stable_cython_alias( - _bindings_source_root(), Path(".cython-bindings-tests") - ) as rel_bindings: - return cythonize( - pyx_files, - include_path=[".", rel_bindings, rel_stdlib], - **cythonize_kwargs, - ) + with ( + build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib, + build_hooks._stable_cython_alias(_bindings_source_root(), Path(".cython-bindings-tests")) as rel_bindings, + ): + return cythonize( + pyx_files, + include_path=[".", rel_bindings, rel_stdlib], + **cythonize_kwargs, + ) def main() -> None: diff --git a/cuda_core/tests/cython/build_tests.py b/cuda_core/tests/cython/build_tests.py index 6518e9a679b..292dd3cb815 100644 --- a/cuda_core/tests/cython/build_tests.py +++ b/cuda_core/tests/cython/build_tests.py @@ -75,15 +75,15 @@ def _cythonize_tests(pyx_files): # .cython-bindings symlinks are not replaced. Relative aliases resolve # next to build_hooks.py (package root). stdlib_target = Path(Cython.__file__).parent / "Includes" - with build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib: - with build_hooks._stable_cython_alias( - _bindings_source_root(), Path(".cython-bindings-tests") - ) as rel_bindings: - return cythonize( - pyx_files, - include_path=[".", rel_bindings, rel_stdlib], - **cythonize_kwargs, - ) + with ( + build_hooks._stable_cython_alias(stdlib_target, Path(".cython-stdlib-tests")) as rel_stdlib, + build_hooks._stable_cython_alias(_bindings_source_root(), Path(".cython-bindings-tests")) as rel_bindings, + ): + return cythonize( + pyx_files, + include_path=[".", rel_bindings, rel_stdlib], + **cythonize_kwargs, + ) def main() -> None: