diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index c5061a16eb..2fe12a3b99 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -96,12 +96,15 @@ jobs: uses: nv-gha-runners/setup-proxy-cache@main continue-on-error: true + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Install dependencies uses: ./.github/actions/install_unix_deps continue-on-error: false with: - # for artifact fetching, graphics libs - dependencies: "jq wget libgl1 libegl1" + # for artifact fetching, graphics libs, g++ required for cffi in example + dependencies: "jq wget libgl1 libegl1 g++" dependent_exes: "jq wget" - name: Set environment variables diff --git a/cuda_core/examples/cuda_graphs.py b/cuda_core/examples/cuda_graphs.py index be23067200..57321dd48c 100644 --- a/cuda_core/examples/cuda_graphs.py +++ b/cuda_core/examples/cuda_graphs.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -10,6 +10,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + import sys import time @@ -121,6 +125,9 @@ def main(): end_time = time.time() graph_execution_time = end_time - start_time + if graph_execution_time == 0.0: + print("Graph execution time is too fast to measure accurately.") + graph_execution_time = 1e-9 # Assign a small value to avoid division by zero in speedup calculation print(f"Graph execution time: {graph_execution_time:.6f} seconds") # Verify results diff --git a/cuda_core/examples/gl_interop_plasma.py b/cuda_core/examples/gl_interop_plasma.py index 3d881a90f2..d303abdc25 100644 --- a/cuda_core/examples/gl_interop_plasma.py +++ b/cuda_core/examples/gl_interop_plasma.py @@ -53,9 +53,10 @@ # effect popular in the demoscene). The window title shows the current FPS. # Close the window or press Escape to exit. # -# Requirements -# ============ -# pip install pyglet + +# /// script +# dependencies = ["cuda_bindings", "cuda_core>0.6.0", "pyglet"] +# /// import ctypes import sys diff --git a/cuda_core/examples/jit_lto_fractal.py b/cuda_core/examples/jit_lto_fractal.py index acf96be0f0..98fd402ee0 100644 --- a/cuda_core/examples/jit_lto_fractal.py +++ b/cuda_core/examples/jit_lto_fractal.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -12,6 +12,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + import argparse import sys diff --git a/cuda_core/examples/memory_ops.py b/cuda_core/examples/memory_ops.py index a53f33d2df..438c40b333 100644 --- a/cuda_core/examples/memory_ops.py +++ b/cuda_core/examples/memory_ops.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -10,6 +10,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + import sys import cupy as cp diff --git a/cuda_core/examples/pytorch_example.py b/cuda_core/examples/pytorch_example.py index 6909272b4d..5826c0a442 100644 --- a/cuda_core/examples/pytorch_example.py +++ b/cuda_core/examples/pytorch_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "torch"] +# /// + import sys import torch diff --git a/cuda_core/examples/saxpy.py b/cuda_core/examples/saxpy.py index 6e5b320f90..85737f84d4 100644 --- a/cuda_core/examples/saxpy.py +++ b/cuda_core/examples/saxpy.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -10,8 +10,17 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + + import sys +from cuda import pathfinder + +print(pathfinder.load_nvidia_dynamic_lib("nvrtc")) + import cupy as cp from cuda.core import Device, LaunchConfig, Program, ProgramOptions, launch diff --git a/cuda_core/examples/show_device_properties.py b/cuda_core/examples/show_device_properties.py index 093b89b331..566f689094 100644 --- a/cuda_core/examples/show_device_properties.py +++ b/cuda_core/examples/show_device_properties.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core"] +# /// + import sys from cuda.core import Device, system diff --git a/cuda_core/examples/simple_multi_gpu_example.py b/cuda_core/examples/simple_multi_gpu_example.py index 236a1cca20..e4d7a1ccfb 100644 --- a/cuda_core/examples/simple_multi_gpu_example.py +++ b/cuda_core/examples/simple_multi_gpu_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "cupy-cuda13x"] +# /// + import sys import cupy as cp diff --git a/cuda_core/examples/strided_memory_view_cpu.py b/cuda_core/examples/strided_memory_view_cpu.py index 8482021c45..3acebac3f1 100644 --- a/cuda_core/examples/strided_memory_view_cpu.py +++ b/cuda_core/examples/strided_memory_view_cpu.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "cffi", "setuptools"] +# /// + import importlib import string import sys diff --git a/cuda_core/examples/strided_memory_view_gpu.py b/cuda_core/examples/strided_memory_view_gpu.py index 0abf5d086e..b481ae8060 100644 --- a/cuda_core/examples/strided_memory_view_gpu.py +++ b/cuda_core/examples/strided_memory_view_gpu.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + import string import sys diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index 495fe882a9..2a4360834e 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -10,6 +10,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core"] +# /// + import os import sys @@ -67,6 +71,7 @@ def main(): cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) if cuda_path is None: + print(os.environ) print("this example requires a valid CUDA_PATH environment variable set", file=sys.stderr) sys.exit(1) cuda_include = os.path.join(cuda_path, "include") diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index b914651089..119e281c85 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -22,6 +22,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core>0.6.0", "cupy-cuda13x"] +# /// + import os import sys diff --git a/cuda_core/examples/vector_add.py b/cuda_core/examples/vector_add.py index 3adf04882e..adb2bebcf8 100644 --- a/cuda_core/examples/vector_add.py +++ b/cuda_core/examples/vector_add.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,10 @@ # # ################################################################################ +# /// script +# dependencies = ["cuda_bindings", "cuda_core", "nvidia-cuda-nvrtc", "cupy-cuda13x"] +# /// + import cupy as cp from cuda.core import Device, LaunchConfig, Program, ProgramOptions, launch diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 9b3e5a37c5..16af61bb37 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -56,7 +56,7 @@ cu12 = ["cuda-bindings[all]==12.*"] cu13 = ["cuda-bindings[all]==13.*"] [dependency-groups] -test = ["cython>=3.2,<3.3", "setuptools", "pytest>=6.2.4", "pytest-randomly", "pytest-repeat", "pytest-rerunfailures"] +test = ["cython>=3.2,<3.3", "setuptools", "pytest>=6.2.4", "pytest-randomly", "pytest-repeat", "pytest-rerunfailures", "cffi"] ml-dtypes = ["ml-dtypes>=0.5.4,<0.6.0"] test-cu12 = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda12x; python_version < '3.14'", "cuda-toolkit[cudart]==12.*"] # runtime headers needed by CuPy test-cu13 = [ {include-group = "ml-dtypes" }, {include-group = "test" }, "cupy-cuda13x; python_version < '3.14'", "cuda-toolkit[cudart]==13.*"] # runtime headers needed by CuPy diff --git a/cuda_core/tests/example_tests/test_basic_examples.py b/cuda_core/tests/example_tests/test_basic_examples.py index 48f16813df..f2d09a5068 100644 --- a/cuda_core/tests/example_tests/test_basic_examples.py +++ b/cuda_core/tests/example_tests/test_basic_examples.py @@ -1,24 +1,153 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # If we have subcategories of examples in the future, this file can be split along those lines import glob +import importlib.metadata import os +import platform +import re +import subprocess +import sys import pytest -from cuda.core import Device +from cuda.core import Device, system + +# Each example in cuda_core/examples is tested in two ways: +# +# 1) Directly running the example in the same environment as the test suite. +# This gives access to the current development version of cuda_core. +# 2) Running the example in a subprocess with "uv run" to verify that the PEP +# 723 metadata works correctly and that the example can be run in isolation from +# the test suite. + + +def has_compute_capability_9_or_higher() -> bool: + return Device().compute_capability >= (9, 0) + + +def has_multiple_devices() -> bool: + return system.get_num_devices() >= 2 + + +def has_display() -> bool: + # We assume that we don't want to open any windows during testing, + # so we always return False + return False + + +def is_not_windows() -> bool: + return sys.platform != "win32" + + +def is_x86_64() -> bool: + return platform.machine() == "x86_64" + + +def uv_installed() -> bool: + try: + subprocess.run(["uv", "--version"], check=True) # noqa: S607 + except (subprocess.CalledProcessError, FileNotFoundError): + return False + return True + + +def has_cuda_path() -> bool: + return os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) is not None + + +# Specific system requirements for each of the examples. + + +SYSTEM_REQUIREMENTS = { + "gl_interop_plasma.py": has_display, + "pytorch_example.py": lambda: ( + has_compute_capability_9_or_higher() and is_x86_64() + ), # PyTorch only provides CUDA support for x86_64 + "saxpy.py": has_compute_capability_9_or_higher, + "simple_multi_gpu_example.py": has_multiple_devices, + "strided_memory_view_cpu.py": is_not_windows, + "thread_block_cluster.py": lambda: has_compute_capability_9_or_higher() and has_cuda_path(), + "tma_tensor_map.py": has_cuda_path, +} -from .utils import run_example samples_path = os.path.join(os.path.dirname(__file__), "..", "..", "examples") -sample_files = glob.glob(samples_path + "**/*.py", recursive=True) +sample_files = [os.path.basename(x) for x in glob.glob(samples_path + "**/*.py", recursive=True)] + + +def has_package_requirements_or_skip(example): + with open(example, encoding="utf-8") as f: + content = f.read() + + # The canonical regex as defined in PEP 723 + pep723 = re.search(r"(?m)^# /// (?P[a-zA-Z0-9-]+)$\s(?P(^#(| .*)$\s)+)^# ///$", content) + if not pep723: + return + + metadata = {} + for line in pep723.group("content").splitlines(): + line = line.lstrip("# ").rstrip() + if not line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + metadata[key] = value + + if "dependencies" in metadata: + dependencies = eval(metadata["dependencies"]) # noqa: S307 + for dependency in dependencies: + name = re.match("[a-zA-Z0-9_-]+", dependency) + try: + importlib.metadata.distribution(name.string) + except importlib.metadata.PackageNotFoundError: + pytest.skip(f"Skipping {example} due to missing package requirement: {name}") + + +@pytest.mark.parametrize("example", sample_files) +def test_example(example): + example_path = os.path.join(samples_path, example) + has_package_requirements_or_skip(example_path) + + system_requirement = SYSTEM_REQUIREMENTS.get(example, lambda: True) + if not system_requirement(): + pytest.skip(f"Skipping {example} due to unmet system requirement") + + process = subprocess.run([sys.executable, example_path], capture_output=True) # noqa: S603 + if process.returncode != 0: + if process.stdout: + print(process.stdout.decode()) + if process.stderr: + print(process.stderr.decode(), file=sys.stderr) + raise AssertionError(f"`{example}` failed ({process.returncode})") @pytest.mark.parametrize("example", sample_files) -class TestExamples: - def test_example(self, example, deinit_cuda): - run_example(samples_path, example) - if Device().device_id != 0: - Device(0).set_current() +@pytest.mark.skipif(not uv_installed(), reason="uv is required to test PEP 723 metadata") +def test_example_pep723(example): + example_path = os.path.join(samples_path, example) + + system_requirement = SYSTEM_REQUIREMENTS.get(example, lambda: True) + if not system_requirement(): + pytest.skip(f"Skipping {example} due to unmet system requirement") + + # Have uv use the same version of Python that is running the test suite, + # not because they have to match but to give Python version coverage in CI. + version_info = sys.version_info + py_version = f"{version_info.major}.{version_info.minor}" + + process = subprocess.run(["uv", "run", "--python", py_version, example_path], capture_output=True) # noqa: S603, S607 + if process.returncode != 0: + # This example requires a development version of cuda_core, so requirements can't be met. + # That's ok, it was tested in the other test, so we just skip it instead of failing. + if re.search("Because only cuda-((core)|(bindings))", process.stderr.decode()): + pytest.skip(f"Skipping {example} due to unmet PEP 723 requirement") + + if process.stdout: + print(process.stdout.decode()) + if process.stderr: + print(process.stderr.decode(), file=sys.stderr) + raise AssertionError(f"`uv run {example}` failed ({process.returncode})") diff --git a/cuda_core/tests/example_tests/utils.py b/cuda_core/tests/example_tests/utils.py deleted file mode 100644 index 9b5dc57e5f..0000000000 --- a/cuda_core/tests/example_tests/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import gc -import os -import sys - -import pytest - - -class SampleTestError(Exception): - pass - - -def parse_python_script(filepath): - if not filepath.endswith(".py"): - raise ValueError(f"{filepath} not supported") - with open(filepath, encoding="utf-8") as f: - script = f.read() - return script - - -def run_example(samples_path, filename, env=None): - fullpath = os.path.join(samples_path, filename) - script = parse_python_script(fullpath) - try: - old_argv = sys.argv - sys.argv = [fullpath] - old_sys_path = sys.path.copy() - sys.path.append(samples_path) - # TODO: Refactor the examples to give them a common callable `main()` to avoid needing to use exec here? - exec(script, env if env else {}) # noqa: S102 - except ImportError as e: - # for samples requiring any of optional dependencies - for m in ("cupy", "torch"): - if f"No module named '{m}'" in str(e): - pytest.skip(f"{m} not installed, skipping related tests") - break - else: - raise - except SystemExit: - # for samples that early return due to any missing requirements - pytest.skip(f"skip {filename}") - except Exception as e: - msg = "\n" - msg += f"Got error ({filename}):\n" - msg += str(e) - raise SampleTestError(msg) from e - finally: - sys.path = old_sys_path - sys.argv = old_argv - # further reduce the memory watermark - gc.collect()